fischer-agentkit/src/agentkit/server/routes/mcp_publish.py

199 lines
6.1 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 发布路由 — 将 Skill / 专家团队发布为 MCP 工具
U14: 管理员通过这些端点把已注册的 Skill 或专家团队封装成 MCP 工具,
注册到 ``app.state.mcp_publisher_registry`` 后即可通过 MCP 端点对外暴露。
所有发布/取消发布/列表端点均要求 ``SYSTEM_CONFIG`` 权限admin
已发布的工具本身通过 ``/api/v1/mcp/tools/list`` 等 member 级端点调用。
"""
from __future__ import annotations
import logging
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, ConfigDict
from agentkit.mcp.publisher import (
PublisherRegistry,
SkillMCPAdapter,
TeamMCPAdapter,
check_dangerous_publish,
)
from agentkit.server.auth.dependencies import require_permission
from agentkit.server.auth.permissions import Permission
logger = logging.getLogger(__name__)
router = APIRouter(tags=["mcp-publish"])
# 发布端点要求 adminSYSTEM_CONFIG权限——防止普通用户把高危能力暴露给外部系统。
_admin_auth = require_permission(Permission.SYSTEM_CONFIG)
class SkillPublishRequest(BaseModel):
"""Skill 发布请求体。"""
model_config = ConfigDict(extra="forbid")
allow_dangerous: bool = False
description_override: str | None = None
class TeamPublishRequest(BaseModel):
"""专家团队发布请求体。"""
model_config = ConfigDict(extra="forbid")
description: str | None = None
allow_dangerous: bool = False
def _get_publisher_registry(request: Request) -> PublisherRegistry:
"""获取 app.state 上的 PublisherRegistry不存在则 500。"""
registry = getattr(request.app.state, "mcp_publisher_registry", None)
if registry is None:
raise HTTPException(
status_code=500,
detail="mcp_publisher_registry not configured on app.state",
)
return registry
def _build_skill_executor(app_state: Any):
"""构造 Skill 执行器闭包。
lazy MVP: 仅当 ``app.state.agent_pool`` 存在且实现了 ``run_skill`` 时调用;
否则返回错误 dict不阻塞发布调用时才报错
ponytail: 完整的 agent 执行接线推迟到后续迭代。
"""
async def _executor(skill_name: str, input_text: str) -> dict[str, Any]:
pool = getattr(app_state, "agent_pool", None)
if pool is None:
return {"error": "agent_pool not available"}
run_skill = getattr(pool, "run_skill", None)
if run_skill is None:
return {"error": "agent_pool.run_skill not implemented"}
return await run_skill(skill_name, input_text)
return _executor
def _build_team_executor(app_state: Any):
"""构造专家团队执行器闭包。"""
async def _executor(team_name: str, input_text: str) -> dict[str, Any]:
pool = getattr(app_state, "agent_pool", None)
if pool is None:
return {"error": "agent_pool not available"}
run_team = getattr(pool, "run_team", None)
if run_team is None:
return {"error": "agent_pool.run_team not implemented"}
return await run_team(team_name, input_text)
return _executor
@router.post("/mcp/publish/skill/{skill_name}")
async def publish_skill(
skill_name: str,
body: SkillPublishRequest,
request: Request,
_user: dict = Depends(_admin_auth),
) -> dict[str, Any]:
"""发布一个 Skill 为 MCP 工具。
- Skill 不存在 → 404
- Skill 含危险工具且未显式 allow_dangerous → 403
- 同名工具已发布 → 409
"""
skill_registry = getattr(request.app.state, "skill_registry", None)
if skill_registry is None:
raise HTTPException(status_code=500, detail="skill_registry not configured")
try:
skill = skill_registry.get(skill_name)
except (KeyError, ValueError, AttributeError):
raise HTTPException(status_code=404, detail=f"Skill '{skill_name}' not found")
try:
check_dangerous_publish(skill, body.allow_dangerous)
except ValueError as e:
raise HTTPException(status_code=403, detail=str(e))
executor = _build_skill_executor(request.app.state)
description = body.description_override or None
adapter = SkillMCPAdapter(skill, executor=executor)
if description is not None:
adapter.description = description
registry = _get_publisher_registry(request)
try:
registry.register(adapter)
except ValueError as e:
raise HTTPException(status_code=409, detail=str(e))
return {"published": adapter.name, "type": "skill"}
@router.post("/mcp/publish/team/{team_name}")
async def publish_team(
team_name: str,
body: TeamPublishRequest,
request: Request,
_user: dict = Depends(_admin_auth),
) -> dict[str, Any]:
"""发布一个专家团队为 MCP 工具。
- 同名工具已发布 → 409
"""
executor = _build_team_executor(request.app.state)
adapter = TeamMCPAdapter(
team_name,
executor=executor,
description=body.description,
)
registry = _get_publisher_registry(request)
try:
registry.register(adapter)
except ValueError as e:
raise HTTPException(status_code=409, detail=str(e))
return {"published": adapter.name, "type": "team"}
@router.delete("/mcp/publish/{name}")
async def unpublish(
name: str,
request: Request,
_user: dict = Depends(_admin_auth),
) -> dict[str, Any]:
"""取消发布一个 MCP 工具。不存在 → 404。"""
registry = _get_publisher_registry(request)
if not registry.unregister(name):
raise HTTPException(status_code=404, detail=f"Published tool '{name}' not found")
return {"unpublished": name}
@router.get("/mcp/publish")
async def list_published(
request: Request,
_user: dict = Depends(_admin_auth),
) -> dict[str, Any]:
"""列出所有已发布的 MCP 工具。"""
registry = _get_publisher_registry(request)
tools = registry.list_published()
return {
"published": [
{
"name": t.name,
"type": t.tags[0] if t.tags else "unknown",
"description": t.description,
}
for t in tools
]
}