Files
mai-bot/src/mcp_module/models.py
DrSmoothl 42dbd5462a feat(mcp_module): add hooks, host LLM bridge, and models for MCP integration
- Introduced MCPHostCallbacks for optional host capabilities like sampling and logging.
- Implemented MCPHostLLMBridge to handle MCP Sampling requests and bridge to LLM service.
- Created models for structured data conversion between MCP SDK and internal data models, including tool content items, prompts, and resources.
- Enhanced error handling and logging for better traceability during sampling operations.
2026-03-30 23:51:05 +08:00

419 lines
13 KiB
Python

"""MCP 结构化模型与转换工具。
负责在 MCP SDK 原始对象与主程序内部数据模型之间进行转换,
避免连接层和管理器层直接操作大量弱类型字段。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Optional
from src.core.tooling import ToolAnnotation, ToolContentItem, ToolIcon
def _dump_model_metadata(raw_value: Any) -> dict[str, Any]:
"""提取任意 MCP 模型对象中的元数据字典。
Args:
raw_value: MCP SDK 返回的原始对象。
Returns:
dict[str, Any]: 归一化后的元数据字典。
"""
metadata = getattr(raw_value, "meta", None)
if isinstance(metadata, dict):
return dict(metadata)
return {}
def build_tool_icon(raw_icon: Any) -> ToolIcon:
"""将 MCP 图标对象转换为统一图标模型。
Args:
raw_icon: MCP SDK 返回的图标对象。
Returns:
ToolIcon: 统一图标模型。
"""
sizes_value = getattr(raw_icon, "sizes", None)
sizes = [str(item) for item in sizes_value] if isinstance(sizes_value, list) else []
return ToolIcon(
src=str(getattr(raw_icon, "src", "") or ""),
mime_type=str(getattr(raw_icon, "mimeType", "") or ""),
sizes=sizes,
)
def build_tool_annotation(raw_annotation: Any) -> Optional[ToolAnnotation]:
"""将 MCP 注解对象转换为统一注解模型。
Args:
raw_annotation: MCP SDK 返回的注解对象。
Returns:
Optional[ToolAnnotation]: 统一注解模型;无有效内容时返回 ``None``。
"""
if raw_annotation is None:
return None
audience_value = getattr(raw_annotation, "audience", None)
audience = [str(item) for item in audience_value] if isinstance(audience_value, list) else []
priority_value = getattr(raw_annotation, "priority", None)
priority = float(priority_value) if isinstance(priority_value, int | float) else None
metadata = _dump_model_metadata(raw_annotation)
if not audience and priority is None and not metadata:
return None
return ToolAnnotation(
audience=audience,
priority=priority,
metadata=metadata,
)
def build_tool_content_item(raw_content: Any) -> ToolContentItem:
"""将 MCP 内容块转换为统一工具内容项。
Args:
raw_content: MCP SDK 返回的内容块对象。
Returns:
ToolContentItem: 统一工具内容项。
"""
content_type = str(getattr(raw_content, "type", "") or "").strip().lower()
annotation = build_tool_annotation(getattr(raw_content, "annotations", None))
metadata = _dump_model_metadata(raw_content)
if content_type == "text" or hasattr(raw_content, "text"):
return ToolContentItem(
content_type="text",
text=str(getattr(raw_content, "text", "") or ""),
annotation=annotation,
metadata=metadata,
)
if content_type == "image":
return ToolContentItem(
content_type="image",
data=str(getattr(raw_content, "data", "") or ""),
mime_type=str(getattr(raw_content, "mimeType", "") or ""),
annotation=annotation,
metadata=metadata,
)
if content_type == "audio":
return ToolContentItem(
content_type="audio",
data=str(getattr(raw_content, "data", "") or ""),
mime_type=str(getattr(raw_content, "mimeType", "") or ""),
annotation=annotation,
metadata=metadata,
)
if content_type == "resource_link":
return ToolContentItem(
content_type="resource_link",
uri=str(getattr(raw_content, "uri", "") or ""),
name=str(getattr(raw_content, "name", "") or ""),
description=str(getattr(raw_content, "description", "") or ""),
mime_type=str(getattr(raw_content, "mimeType", "") or ""),
annotation=annotation,
metadata=metadata,
)
if content_type == "resource" or hasattr(raw_content, "resource"):
resource = getattr(raw_content, "resource", None)
resource_metadata = metadata | _dump_model_metadata(resource)
return ToolContentItem(
content_type="resource",
text=str(getattr(resource, "text", "") or ""),
data=str(getattr(resource, "blob", "") or ""),
mime_type=str(getattr(resource, "mimeType", "") or ""),
uri=str(getattr(resource, "uri", "") or ""),
name=str(getattr(resource, "name", "") or ""),
annotation=annotation,
metadata=resource_metadata,
)
if hasattr(raw_content, "data"):
return ToolContentItem(
content_type="binary",
data=str(getattr(raw_content, "data", "") or ""),
mime_type=str(getattr(raw_content, "mimeType", "") or ""),
annotation=annotation,
metadata=metadata,
)
return ToolContentItem(
content_type="unknown",
text=str(raw_content),
annotation=annotation,
metadata=metadata,
)
def build_tool_content_items(raw_contents: list[Any] | None) -> list[ToolContentItem]:
"""批量转换 MCP 内容块列表。
Args:
raw_contents: MCP SDK 返回的内容块列表。
Returns:
list[ToolContentItem]: 转换后的统一内容项列表。
"""
if not raw_contents:
return []
return [build_tool_content_item(item) for item in raw_contents]
@dataclass(slots=True)
class MCPPromptArgumentSpec:
"""MCP Prompt 参数声明。"""
name: str
description: str = ""
required: bool = False
@dataclass(slots=True)
class MCPPromptSpec:
"""MCP Prompt 声明。"""
name: str
server_name: str
title: str = ""
description: str = ""
arguments: list[MCPPromptArgumentSpec] = field(default_factory=list)
icons: list[ToolIcon] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(slots=True)
class MCPPromptMessage:
"""MCP Prompt 消息。"""
role: str
content: ToolContentItem
@dataclass(slots=True)
class MCPPromptResult:
"""MCP Prompt 获取结果。"""
prompt_name: str
server_name: str
description: str = ""
messages: list[MCPPromptMessage] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(slots=True)
class MCPResourceSpec:
"""MCP Resource 声明。"""
uri: str
server_name: str
name: str
title: str = ""
description: str = ""
mime_type: str = ""
size: int | None = None
icons: list[ToolIcon] = field(default_factory=list)
annotation: ToolAnnotation | None = None
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(slots=True)
class MCPResourceTemplateSpec:
"""MCP Resource Template 声明。"""
uri_template: str
server_name: str
name: str
title: str = ""
description: str = ""
mime_type: str = ""
icons: list[ToolIcon] = field(default_factory=list)
annotation: ToolAnnotation | None = None
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(slots=True)
class MCPResourceReadResult:
"""MCP Resource 读取结果。"""
uri: str
server_name: str
contents: list[ToolContentItem] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def build_prompt_argument_spec(raw_argument: Any) -> MCPPromptArgumentSpec:
"""将 MCP Prompt 参数对象转换为统一结构。
Args:
raw_argument: MCP SDK 返回的 Prompt 参数对象。
Returns:
MCPPromptArgumentSpec: 统一 Prompt 参数结构。
"""
return MCPPromptArgumentSpec(
name=str(getattr(raw_argument, "name", "") or ""),
description=str(getattr(raw_argument, "description", "") or ""),
required=bool(getattr(raw_argument, "required", False)),
)
def build_prompt_spec(raw_prompt: Any, server_name: str) -> MCPPromptSpec:
"""将 MCP Prompt 定义转换为统一结构。
Args:
raw_prompt: MCP SDK 返回的 Prompt 对象。
server_name: Prompt 所属的服务器名称。
Returns:
MCPPromptSpec: 统一 Prompt 定义。
"""
raw_arguments = getattr(raw_prompt, "arguments", None)
raw_icons = getattr(raw_prompt, "icons", None)
return MCPPromptSpec(
name=str(getattr(raw_prompt, "name", "") or ""),
server_name=server_name,
title=str(getattr(raw_prompt, "title", "") or ""),
description=str(getattr(raw_prompt, "description", "") or ""),
arguments=[build_prompt_argument_spec(item) for item in raw_arguments] if isinstance(raw_arguments, list) else [],
icons=[build_tool_icon(item) for item in raw_icons] if isinstance(raw_icons, list) else [],
metadata=_dump_model_metadata(raw_prompt),
)
def build_prompt_result(raw_result: Any, prompt_name: str, server_name: str) -> MCPPromptResult:
"""将 MCP Prompt 获取结果转换为统一结构。
Args:
raw_result: MCP SDK 返回的 Prompt 结果对象。
prompt_name: Prompt 名称。
server_name: Prompt 所属服务器名称。
Returns:
MCPPromptResult: 统一 Prompt 获取结果。
"""
messages: list[MCPPromptMessage] = []
raw_messages = getattr(raw_result, "messages", None)
if isinstance(raw_messages, list):
for raw_message in raw_messages:
messages.append(
MCPPromptMessage(
role=str(getattr(raw_message, "role", "") or ""),
content=build_tool_content_item(getattr(raw_message, "content", None)),
)
)
return MCPPromptResult(
prompt_name=prompt_name,
server_name=server_name,
description=str(getattr(raw_result, "description", "") or ""),
messages=messages,
metadata=_dump_model_metadata(raw_result),
)
def build_resource_spec(raw_resource: Any, server_name: str) -> MCPResourceSpec:
"""将 MCP Resource 定义转换为统一结构。
Args:
raw_resource: MCP SDK 返回的 Resource 对象。
server_name: Resource 所属服务器名称。
Returns:
MCPResourceSpec: 统一 Resource 定义。
"""
raw_icons = getattr(raw_resource, "icons", None)
size_value = getattr(raw_resource, "size", None)
size = int(size_value) if isinstance(size_value, int | float) else None
return MCPResourceSpec(
uri=str(getattr(raw_resource, "uri", "") or ""),
server_name=server_name,
name=str(getattr(raw_resource, "name", "") or ""),
title=str(getattr(raw_resource, "title", "") or ""),
description=str(getattr(raw_resource, "description", "") or ""),
mime_type=str(getattr(raw_resource, "mimeType", "") or ""),
size=size,
icons=[build_tool_icon(item) for item in raw_icons] if isinstance(raw_icons, list) else [],
annotation=build_tool_annotation(getattr(raw_resource, "annotations", None)),
metadata=_dump_model_metadata(raw_resource),
)
def build_resource_template_spec(raw_template: Any, server_name: str) -> MCPResourceTemplateSpec:
"""将 MCP Resource Template 定义转换为统一结构。
Args:
raw_template: MCP SDK 返回的 ResourceTemplate 对象。
server_name: 模板所属服务器名称。
Returns:
MCPResourceTemplateSpec: 统一模板定义。
"""
raw_icons = getattr(raw_template, "icons", None)
return MCPResourceTemplateSpec(
uri_template=str(getattr(raw_template, "uriTemplate", "") or ""),
server_name=server_name,
name=str(getattr(raw_template, "name", "") or ""),
title=str(getattr(raw_template, "title", "") or ""),
description=str(getattr(raw_template, "description", "") or ""),
mime_type=str(getattr(raw_template, "mimeType", "") or ""),
icons=[build_tool_icon(item) for item in raw_icons] if isinstance(raw_icons, list) else [],
annotation=build_tool_annotation(getattr(raw_template, "annotations", None)),
metadata=_dump_model_metadata(raw_template),
)
def build_resource_read_result(raw_result: Any, uri: str, server_name: str) -> MCPResourceReadResult:
"""将 MCP Resource 读取结果转换为统一结构。
Args:
raw_result: MCP SDK 返回的读取结果对象。
uri: 被读取的资源 URI。
server_name: 资源所属服务器名称。
Returns:
MCPResourceReadResult: 统一资源读取结果。
"""
contents: list[ToolContentItem] = []
raw_contents = getattr(raw_result, "contents", None)
if isinstance(raw_contents, list):
for raw_content in raw_contents:
metadata = _dump_model_metadata(raw_content)
contents.append(
ToolContentItem(
content_type="resource",
text=str(getattr(raw_content, "text", "") or ""),
data=str(getattr(raw_content, "blob", "") or ""),
mime_type=str(getattr(raw_content, "mimeType", "") or ""),
uri=str(getattr(raw_content, "uri", "") or uri),
annotation=None,
metadata=metadata,
)
)
return MCPResourceReadResult(
uri=uri,
server_name=server_name,
contents=contents,
metadata=_dump_model_metadata(raw_result),
)