fischer-agentkit/src/agentkit/mcp/publisher.py

151 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""MCP Publisher — 将 Skill / 专家团队发布为 MCP 工具
U14: 支持把已注册的 Skill 或专家团队封装成 ``Tool``,注册到
``PublisherRegistry`` 后即可通过 MCP 端点(``/api/v1/mcp/tools/list``、
``/tools/call``、JSON-RPC对外暴露供外部系统调用。
设计要点lazy MVP
- ``SkillMCPAdapter`` / ``TeamMCPAdapter`` 通过可选的 ``executor``
回调执行真正的技能/团队逻辑;未配置时返回错误 dict不阻塞发布。
- 危险工具terminal/shell/file_*)默认拒绝发布,需显式 ``allow_dangerous=True``
才能放行——防止把高危能力意外暴露给外部系统。
"""
from __future__ import annotations
import logging
from typing import Any, Awaitable, Callable
from agentkit.skills.base import Skill
from agentkit.tools.base import Tool
logger = logging.getLogger(__name__)
# 危险工具名集合——这些工具若被发布到 MCP外部系统可直接操作终端/文件系统,
# 故默认拒绝发布,需管理员显式 opt-in。ponytail: 集合较小,硬编码即可。
_DANGEROUS_TOOL_NAMES: frozenset[str] = frozenset(
{"terminal", "shell", "file_write", "file_read", "file_delete"}
)
# 执行器签名:(skill_or_team_name, input_text) -> 结果 dict
SkillExecutor = Callable[[str, str], Awaitable[dict[str, Any]]]
class PublisherRegistry:
"""已发布 MCP 工具的内存注册表。
进程内注册表——重启后丢失MVP 不持久化,足够当前场景)。
按工具名去重,重复注册抛 ``ValueError``。
"""
def __init__(self) -> None:
self._tools: dict[str, Tool] = {}
def register(self, tool: Tool) -> None:
"""注册一个已发布工具。同名工具已存在则抛 ValueError。"""
if tool.name in self._tools:
raise ValueError(f"Tool '{tool.name}' already published")
self._tools[tool.name] = tool
logger.info(f"MCP tool published: {tool.name}")
def unregister(self, name: str) -> bool:
"""注销已发布工具。成功返回 True不存在返回 False。"""
if name in self._tools:
del self._tools[name]
logger.info(f"MCP tool unpublished: {name}")
return True
return False
def list_published(self) -> list[Tool]:
"""返回所有已发布工具(按注册顺序)。"""
return list(self._tools.values())
def get(self, name: str) -> Tool | None:
"""按名获取已发布工具,不存在返回 None。"""
return self._tools.get(name)
class SkillMCPAdapter(Tool):
"""将 Skill 封装为 MCP 工具。
工具名格式:``skill_{skill.name}``。
真正的技能执行通过 ``executor`` 回调完成;未配置时 execute 返回错误信息。
"""
def __init__(
self,
skill: Skill,
executor: SkillExecutor | None = None,
) -> None:
super().__init__(
name=f"skill_{skill.name}",
description=skill.config.description or f"Skill: {skill.name}",
input_schema={
"type": "object",
"properties": {"input": {"type": "string", "description": "技能输入"}},
"required": ["input"],
},
tags=["skill", skill.name],
)
self._skill = skill
self._executor = executor
async def execute(self, **kwargs: Any) -> dict[str, Any]:
"""调用 executor 执行技能;未配置或异常时返回错误 dict。"""
input_text = kwargs.get("input", "")
if self._executor is None:
return {"error": f"skill '{self._skill.name}' executor not configured"}
try:
return await self._executor(self._skill.name, input_text)
except Exception as e:
return {"error": f"skill execution failed: {e}"}
class TeamMCPAdapter(Tool):
"""将专家团队封装为 MCP 工具。
工具名格式:``team_{team_name}``。
"""
def __init__(
self,
team_name: str,
executor: SkillExecutor | None = None,
description: str | None = None,
) -> None:
super().__init__(
name=f"team_{team_name}",
description=description or f"Expert team: {team_name}",
input_schema={
"type": "object",
"properties": {"input": {"type": "string", "description": "团队任务输入"}},
"required": ["input"],
},
tags=["team", team_name],
)
self._team_name = team_name
self._executor = executor
async def execute(self, **kwargs: Any) -> dict[str, Any]:
"""调用 executor 执行团队任务;未配置或异常时返回错误 dict。"""
input_text = kwargs.get("input", "")
if self._executor is None:
return {"error": f"team '{self._team_name}' executor not configured"}
try:
return await self._executor(self._team_name, input_text)
except Exception as e:
return {"error": f"team execution failed: {e}"}
def check_dangerous_publish(skill: Skill, allow_dangerous: bool) -> None:
"""检查 Skill 绑定的工具是否包含危险工具。
若包含危险工具且未显式 ``allow_dangerous=True``,抛 ``ValueError``。
"""
dangerous = [t.name for t in skill.tools if t.name in _DANGEROUS_TOOL_NAMES]
if dangerous and not allow_dangerous:
raise ValueError(
f"Skill '{skill.name}' contains dangerous tools {dangerous}; "
"publish requires allow_dangerous=True"
)