feat: 重构 MCP 配置管理,移除旧配置文件,更新为使用全局配置

This commit is contained in:
DrSmoothl
2026-03-30 23:32:13 +08:00
parent dc2bf02a42
commit abb1d071b1
10 changed files with 214 additions and 135 deletions

View File

@@ -1,13 +0,0 @@
{
"mcpServers": {
"tavily": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://mcp.tavily.com/mcp/?tavilyApiKey=YOUR_API_KEY_HERE"
],
"env": {}
}
}
}

View File

@@ -3,7 +3,6 @@ MaiSaka CLI and conversation loop.
""" """
from datetime import datetime from datetime import datetime
from pathlib import Path
from typing import Optional from typing import Optional
import asyncio import asyncio
@@ -259,7 +258,7 @@ class BufferCLI:
knowledge_result = results[0] if results else None knowledge_result = results[0] if results else None
if isinstance(knowledge_result, Exception): if isinstance(knowledge_result, Exception):
console.print(f"[warning]知识分析失败:{knowledge_result}[/warning]") console.print(f"[warning]知识分析失败:{knowledge_result}[/warning]")
elif knowledge_result: elif isinstance(knowledge_result, str) and knowledge_result.strip():
knowledge_analysis = knowledge_result knowledge_analysis = knowledge_result
if global_config.maisaka.show_thinking: if global_config.maisaka.show_thinking:
console.print( console.print(
@@ -333,7 +332,7 @@ class BufferCLI:
should_stop = True should_stop = True
elif tool_call.func_name == "reply": elif tool_call.func_name == "reply":
reply = await self._generate_visible_reply(chat_history, response.content) reply = await self._generate_visible_reply(chat_history, response.content or "")
chat_history.append( chat_history.append(
ToolResultMessage( ToolResultMessage(
content="已生成并记录可见回复。", content="已生成并记录可见回复。",
@@ -384,8 +383,7 @@ class BufferCLI:
async def _init_mcp(self) -> None: async def _init_mcp(self) -> None:
"""初始化 MCP 服务并注册暴露的工具。""" """初始化 MCP 服务并注册暴露的工具。"""
config_path = Path(__file__).resolve().parents[2] / "config" / "mcp_config.json" self._mcp_manager = await MCPManager.from_app_config(global_config.mcp)
self._mcp_manager = await MCPManager.from_config(str(config_path))
if self._mcp_manager and self._chat_loop_service: if self._mcp_manager and self._chat_loop_service:
mcp_tools = self._mcp_manager.get_openai_tools() mcp_tools = self._mcp_manager.get_openai_tools()
@@ -429,10 +427,10 @@ class BufferCLI:
async def run(self) -> None: async def run(self) -> None:
"""主交互循环。""" """主交互循环。"""
if global_config.maisaka.enable_mcp: if global_config.mcp.enable:
await self._init_mcp() await self._init_mcp()
else: else:
console.print("[muted]MCP 已禁用(ENABLE_MCP=false[/muted]") console.print("[muted]MCP 已禁用(mcp.enable=false[/muted]")
self._reader.start(asyncio.get_event_loop()) self._reader.start(asyncio.get_event_loop())
self._show_banner() self._show_banner()

View File

@@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Mapping, Sequence, TypeVar from typing import Any, Callable, Mapping, Sequence, TypeVar, cast
import asyncio import asyncio
import copy import copy
@@ -27,6 +27,7 @@ from .official_configs import (
LPMMKnowledgeConfig, LPMMKnowledgeConfig,
MaiSakaConfig, MaiSakaConfig,
MaimMessageConfig, MaimMessageConfig,
MCPConfig,
PluginRuntimeConfig, PluginRuntimeConfig,
MemoryConfig, MemoryConfig,
MessageReceiveConfig, MessageReceiveConfig,
@@ -56,7 +57,7 @@ CONFIG_DIR: Path = PROJECT_ROOT / "config"
BOT_CONFIG_PATH: Path = (CONFIG_DIR / "bot_config.toml").resolve().absolute() BOT_CONFIG_PATH: Path = (CONFIG_DIR / "bot_config.toml").resolve().absolute()
MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute() MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute()
MMC_VERSION: str = "1.0.0" MMC_VERSION: str = "1.0.0"
CONFIG_VERSION: str = "8.1.11" CONFIG_VERSION: str = "8.2.0"
MODEL_CONFIG_VERSION: str = "1.13.1" MODEL_CONFIG_VERSION: str = "1.13.1"
logger = get_logger("config") logger = get_logger("config")
@@ -134,6 +135,9 @@ class Config(ConfigBase):
maisaka: MaiSakaConfig = Field(default_factory=MaiSakaConfig) maisaka: MaiSakaConfig = Field(default_factory=MaiSakaConfig)
"""MaiSaka对话系统配置类""" """MaiSaka对话系统配置类"""
mcp: MCPConfig = Field(default_factory=MCPConfig)
"""MCP 配置类"""
plugin_runtime: PluginRuntimeConfig = Field(default_factory=PluginRuntimeConfig) plugin_runtime: PluginRuntimeConfig = Field(default_factory=PluginRuntimeConfig)
"""插件运行时配置类""" """插件运行时配置类"""
@@ -332,7 +336,12 @@ class ConfigManager:
changed_scopes: 本次热重载命中的配置范围。 changed_scopes: 本次热重载命中的配置范围。
""" """
result = callback(changed_scopes) if self._callback_accepts_scopes(callback) else callback() if self._callback_accepts_scopes(callback):
callback_with_scopes = cast(Callable[[Sequence[str]], object], callback)
result = callback_with_scopes(changed_scopes)
else:
callback_without_scopes = cast(Callable[[], object], callback)
result = callback_without_scopes()
if asyncio.iscoroutine(result): if asyncio.iscoroutine(result):
await result await result

View File

@@ -1,6 +1,7 @@
from .config_base import ConfigBase, Field
import re import re
from typing import Optional, Literal from typing import Literal, Optional
from .config_base import ConfigBase, Field
""" """
须知: 须知:
@@ -1493,15 +1494,6 @@ class MaiSakaConfig(ConfigBase):
) )
"""启用知识库模块""" """启用知识库模块"""
enable_mcp: bool = Field(
default=True,
json_schema_extra={
"x-widget": "switch",
"x-icon": "zap",
},
)
"""启用 MCP (Model Context Protocol) 支持"""
show_analyze_cognition_prompt: bool = Field( show_analyze_cognition_prompt: bool = Field(
default=False, default=False,
json_schema_extra={ json_schema_extra={
@@ -1577,6 +1569,128 @@ class MaiSakaConfig(ConfigBase):
"""Maisaka终端图片预览的字符宽度""" """Maisaka终端图片预览的字符宽度"""
class MCPServerItemConfig(ConfigBase):
"""单个 MCP 服务器配置。"""
name: str = Field(
default="",
json_schema_extra={
"x-widget": "input",
"x-icon": "tag",
},
)
"""服务器名称,必须唯一"""
enabled: bool = Field(
default=True,
json_schema_extra={
"x-widget": "switch",
"x-icon": "power",
},
)
"""是否启用当前 MCP 服务器"""
transport: Literal["stdio", "sse"] = Field(
default="stdio",
json_schema_extra={
"x-widget": "select",
"x-icon": "shuffle",
},
)
"""传输方式,可选 stdio 或 sse"""
command: str = Field(
default="",
json_schema_extra={
"x-widget": "input",
"x-icon": "terminal",
},
)
"""stdio 模式下启动服务器的命令"""
args: list[str] = Field(
default_factory=lambda: [],
json_schema_extra={
"x-widget": "custom",
"x-icon": "list",
},
)
"""stdio 模式下的命令参数列表"""
env: dict[str, str] = Field(
default_factory=lambda: {},
json_schema_extra={
"x-widget": "custom",
"x-icon": "variable",
},
)
"""stdio 模式下附加的环境变量"""
url: str = Field(
default="",
json_schema_extra={
"x-widget": "input",
"x-icon": "link",
},
)
"""sse 模式下的服务地址"""
headers: dict[str, str] = Field(
default_factory=lambda: {},
json_schema_extra={
"x-widget": "custom",
"x-icon": "file-json",
},
)
"""sse 模式下附加的请求头"""
def model_post_init(self, context: Optional[dict] = None) -> None:
"""验证 MCP 服务器配置。"""
if not self.name.strip():
raise ValueError("MCPServerItemConfig.name 不能为空")
if self.transport == "stdio" and not self.command.strip():
raise ValueError(f"MCP 服务器 {self.name} 使用 stdio 时必须填写 command")
if self.transport == "sse" and not self.url.strip():
raise ValueError(f"MCP 服务器 {self.name} 使用 sse 时必须填写 url")
return super().model_post_init(context)
class MCPConfig(ConfigBase):
"""MCP 总配置。"""
__ui_parent__ = "maisaka"
enable: bool = Field(
default=True,
json_schema_extra={
"x-widget": "switch",
"x-icon": "zap",
},
)
"""是否启用 MCPModel Context Protocol"""
servers: list[MCPServerItemConfig] = Field(
default_factory=lambda: [],
json_schema_extra={
"x-widget": "custom",
"x-icon": "server",
},
)
"""_wrap_MCP 服务器配置列表"""
def model_post_init(self, context: Optional[dict] = None) -> None:
"""验证 MCP 总配置。"""
server_names = [server.name.strip() for server in self.servers if server.name.strip()]
if len(server_names) != len(set(server_names)):
raise ValueError("MCP 配置中的服务器名称不能重复")
return super().model_post_init(context)
class PluginRuntimeConfig(ConfigBase): class PluginRuntimeConfig(ConfigBase):
"""插件运行时配置类""" """插件运行时配置类"""

View File

@@ -5,7 +5,7 @@ from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from io import BytesIO from io import BytesIO
from time import perf_counter from time import perf_counter
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Sequence
import asyncio import asyncio
import random import random
@@ -133,14 +133,14 @@ class MaisakaChatLoopService:
self._prompts_loaded = True self._prompts_loaded = True
def set_extra_tools(self, tools: List[ToolDefinitionInput]) -> None: def set_extra_tools(self, tools: Sequence[ToolDefinitionInput]) -> None:
"""设置额外工具定义。 """设置额外工具定义。
Args: Args:
tools: 兼容旧接口的额外工具定义列表。 tools: 兼容旧接口的额外工具定义列表。
""" """
self._extra_tools = normalize_tool_options(tools) or [] self._extra_tools = normalize_tool_options(list(tools)) or []
def set_tool_registry(self, tool_registry: ToolRegistry | None) -> None: def set_tool_registry(self, tool_registry: ToolRegistry | None) -> None:
"""设置统一工具注册表。 """设置统一工具注册表。

View File

@@ -1,6 +1,5 @@
"""Maisaka 非 CLI 运行时。""" """Maisaka 非 CLI 运行时。"""
from pathlib import Path
from typing import Literal, Optional from typing import Literal, Optional
import asyncio import asyncio
@@ -91,7 +90,7 @@ class MaisakaHeartFlowChatting:
self._ensure_background_tasks_running() self._ensure_background_tasks_running()
return return
if global_config.maisaka.enable_mcp: if global_config.mcp.enable:
await self._init_mcp() await self._init_mcp()
self._running = True self._running = True
@@ -386,8 +385,7 @@ class MaisakaHeartFlowChatting:
async def _init_mcp(self) -> None: async def _init_mcp(self) -> None:
"""初始化 MCP 工具并注册到统一工具层。""" """初始化 MCP 工具并注册到统一工具层。"""
config_path = Path(__file__).resolve().parents[2] / "config" / "mcp_config.json" self._mcp_manager = await MCPManager.from_app_config(global_config.mcp)
self._mcp_manager = await MCPManager.from_config(str(config_path))
if self._mcp_manager is None: if self._mcp_manager is None:
logger.info(f"{self.log_prefix} MCP 管理器不可用") logger.info(f"{self.log_prefix} MCP 管理器不可用")
return return

View File

@@ -4,9 +4,10 @@ MCP (Model Context Protocol) 客户端包。
提供 MCPManager 用于管理 MCP 服务器连接、发现工具、调用工具。 提供 MCPManager 用于管理 MCP 服务器连接、发现工具、调用工具。
用法: 用法:
from src.config.config import global_config
from .manager import MCPManager from .manager import MCPManager
manager = await MCPManager.from_config("config/mcp_config.json") manager = await MCPManager.from_app_config(global_config.mcp)
if manager: if manager:
tools = manager.get_openai_tools() # 获取 OpenAI 格式工具列表 tools = manager.get_openai_tools() # 获取 OpenAI 格式工具列表
result = await manager.call_tool(name, args) # 调用工具 result = await manager.call_tool(name, args) # 调用工具

View File

@@ -1,56 +1,36 @@
""" """MCP 运行时配置转换。
MCP 配置加载与验证。
从 config/mcp_config.json 读取 MCP 服务器定义,解析为结构化配置对象。
配置格式示例: 负责将主程序官方配置中的 MCP 配置转换为运行时使用的结构化对象。
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "C:/Users"],
"env": {}
},
"remote-api": {
"url": "http://localhost:8080/sse",
"headers": {"Authorization": "Bearer xxx"}
}
}
}
- command + args: Stdio 传输(启动子进程)
- url: SSE 传输(连接远程服务器)
""" """
from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from typing import TYPE_CHECKING
from typing import Optional
import json
import os
from src.cli.console import console if TYPE_CHECKING:
from src.config.official_configs import MCPConfig
DEFAULT_MCP_CONFIG_PATH = Path(__file__).resolve().parents[2] / "config" / "mcp_config.json" @dataclass(slots=True)
class MCPServerRuntimeConfig:
"""单个 MCP 服务器的运行时配置。"""
@dataclass
class MCPServerConfig:
"""单个 MCP 服务器配置。"""
name: str name: str
command: str = ""
# ── Stdio 传输 ──
command: Optional[str] = None
args: list[str] = field(default_factory=list) args: list[str] = field(default_factory=list)
env: Optional[dict[str, str]] = None env: dict[str, str] = field(default_factory=dict)
url: str = ""
# ── SSE 传输 ──
url: Optional[str] = None
headers: dict[str, str] = field(default_factory=dict) headers: dict[str, str] = field(default_factory=dict)
@property @property
def transport_type(self) -> str: def transport_type(self) -> str:
"""返回传输类型: 'stdio' / 'sse' / 'unknown'""" """返回当前服务器的传输类型。
Returns:
str: ``stdio``、``sse`` 或 ``unknown``。
"""
if self.command: if self.command:
return "stdio" return "stdio"
if self.url: if self.url:
@@ -58,50 +38,33 @@ class MCPServerConfig:
return "unknown" return "unknown"
def load_mcp_config(config_path: str = str(DEFAULT_MCP_CONFIG_PATH)) -> list[MCPServerConfig]: def build_mcp_server_runtime_configs(mcp_config: "MCPConfig") -> list[MCPServerRuntimeConfig]:
""" """将官方 MCP 配置转换为运行时配置列表。
从配置文件加载 MCP 服务器列表。
Args: Args:
config_path: 配置文件路径 mcp_config: 主程序中的 MCP 官方配置对象。
Returns: Returns:
解析后的 MCPServerConfig 列表;文件不存在或为空时返回空列表。 list[MCPServerRuntimeConfig]: 启用且配置完整的 MCP 服务器列表。
""" """
if not os.path.isfile(config_path):
if not mcp_config.enable:
return [] return []
try: runtime_configs: list[MCPServerRuntimeConfig] = []
with open(config_path, "r", encoding="utf-8") as f: for server in mcp_config.servers:
data = json.load(f) if not server.enabled:
except (json.JSONDecodeError, OSError) as e:
console.print(f"[warning]⚠️ 读取 MCP 配置失败: {e}[/warning]")
return []
mcp_servers = data.get("mcpServers", {})
if not isinstance(mcp_servers, dict):
console.print("[warning]⚠️ MCP 配置中的 mcpServers 格式无效[/warning]")
return []
configs: list[MCPServerConfig] = []
for name, cfg in mcp_servers.items():
if not isinstance(cfg, dict):
console.print(f"[warning]⚠️ MCP 服务器 '{name}' 配置格式无效,已跳过[/warning]")
continue continue
server = MCPServerConfig( runtime_configs.append(
name=name, MCPServerRuntimeConfig(
command=cfg.get("command"), name=server.name.strip(),
args=cfg.get("args", []), command=server.command.strip(),
env=cfg.get("env"), args=[str(argument) for argument in server.args],
url=cfg.get("url"), env={str(key): str(value) for key, value in server.env.items()},
headers=cfg.get("headers", {}), url=server.url.strip(),
headers={str(key): str(value) for key, value in server.headers.items()},
)
) )
if server.transport_type == "unknown": return runtime_configs
console.print(f"[warning]⚠️ MCP 服务器 '{name}' 缺少 command 或 url已跳过[/warning]")
continue
configs.append(server)
return configs

View File

@@ -10,12 +10,12 @@ from src.core.tooling import ToolExecutionResult
from src.cli.console import console from src.cli.console import console
from .config import MCPServerConfig from .config import MCPServerRuntimeConfig
# ──────────────────── MCP SDK 可选导入 ──────────────────── # ──────────────────── MCP SDK 可选导入 ────────────────────
# #
# mcp 是可选依赖。如果未安装MCP_AVAILABLE = False # mcp 是可选依赖。如果未安装MCP_AVAILABLE = False
# MCPManager.from_config() 会检测到并返回 None不影响主程序运行。 # MCPManager.from_app_config() 会检测到并返回 None不影响主程序运行。
try: try:
from mcp import ClientSession from mcp import ClientSession
@@ -44,15 +44,20 @@ except ImportError:
class MCPConnection: class MCPConnection:
""" """管理单个 MCP 服务器的连接生命周期。
管理单个 MCP 服务器的连接生命周期。
支持两种传输方式: 支持两种传输方式:
- Stdio: 启动子进程,通过 stdin/stdout 通信 - Stdio: 启动子进程,通过 stdin/stdout 通信
- SSE: 连接远程 HTTP SSE 端点 - SSE: 连接远程 HTTP SSE 端点
""" """
def __init__(self, config: MCPServerConfig): def __init__(self, config: MCPServerRuntimeConfig) -> None:
"""初始化单个 MCP 连接。
Args:
config: 当前服务器的运行时配置。
"""
self.config = config self.config = config
self.session: Optional[Any] = None # mcp.ClientSession self.session: Optional[Any] = None # mcp.ClientSession
self.tools: list = [] # mcp Tool objects self.tools: list = [] # mcp Tool objects

View File

@@ -3,7 +3,7 @@ MaiSaka - MCP 管理器
管理所有 MCP 服务器连接,提供统一的工具发现与调用接口。 管理所有 MCP 服务器连接,提供统一的工具发现与调用接口。
""" """
from typing import Any, Optional from typing import TYPE_CHECKING, Any, Optional
from src.cli.console import console from src.cli.console import console
from src.core.tooling import ( from src.core.tooling import (
@@ -13,9 +13,12 @@ from src.core.tooling import (
build_tool_detailed_description, build_tool_detailed_description,
) )
from .config import DEFAULT_MCP_CONFIG_PATH, MCPServerConfig, load_mcp_config from .config import MCPServerRuntimeConfig, build_mcp_server_runtime_configs
from .connection import MCPConnection, MCP_AVAILABLE from .connection import MCPConnection, MCP_AVAILABLE
if TYPE_CHECKING:
from src.config.official_configs import MCPConfig
# 内置工具名称集合 —— MCP 工具不允许与这些名称冲突 # 内置工具名称集合 —— MCP 工具不允许与这些名称冲突
BUILTIN_TOOL_NAMES = frozenset( BUILTIN_TOOL_NAMES = frozenset(
{ {
@@ -31,37 +34,38 @@ BUILTIN_TOOL_NAMES = frozenset(
class MCPManager: class MCPManager:
""" """MCP 服务器连接管理器。
MCP 服务器连接管理器。
职责: 职责:
- 根据配置文件连接所有 MCP 服务器 - 根据主程序官方配置连接所有 MCP 服务器
- 将 MCP 工具转换为 OpenAI function calling 格式 - 将 MCP 工具转换为 OpenAI function calling 格式
- 路由工具调用到正确的 MCP 服务器 - 路由工具调用到正确的 MCP 服务器
- 统一管理连接生命周期 - 统一管理连接生命周期
""" """
def __init__(self): def __init__(self) -> None:
"""初始化 MCP 管理器。"""
self._connections: dict[str, MCPConnection] = {} # server_name → connection self._connections: dict[str, MCPConnection] = {} # server_name → connection
self._tool_to_server: dict[str, str] = {} # tool_name → server_name self._tool_to_server: dict[str, str] = {} # tool_name → server_name
# ──────── 工厂方法 ──────── # ──────── 工厂方法 ────────
@classmethod @classmethod
async def from_config( async def from_app_config(
cls, cls,
config_path: str = str(DEFAULT_MCP_CONFIG_PATH), mcp_config: "MCPConfig",
) -> Optional["MCPManager"]: ) -> Optional["MCPManager"]:
""" """
从配置文件创建并初始化 MCPManager。 官方配置创建并初始化 MCPManager。
Args: Args:
config_path: mcp_config.json 文件路径 mcp_config: 主程序中的 MCP 配置对象。
Returns: Returns:
初始化完成的 MCPManager无配置或全部连接失败时返回 None。 初始化完成的 MCPManager可用配置或全部连接失败时返回 None。
""" """
configs = load_mcp_config(config_path) configs = build_mcp_server_runtime_configs(mcp_config)
if not configs: if not configs:
return None return None
@@ -80,7 +84,7 @@ class MCPManager:
# ──────── 连接管理 ──────── # ──────── 连接管理 ────────
async def _connect_all(self, configs: list[MCPServerConfig]) -> None: async def _connect_all(self, configs: list[MCPServerRuntimeConfig]) -> None:
"""连接所有配置的 MCP 服务器,跳过失败的连接。""" """连接所有配置的 MCP 服务器,跳过失败的连接。"""
for cfg in configs: for cfg in configs:
conn = MCPConnection(cfg) conn = MCPConnection(cfg)