"""MCP Server:向 Agent 暴露工具。 强制流程:list_providers() → discover_models(provider_id) → generate_image(...) matched 为空时,Agent 必须直接回复「没找到有效的模型」。 """ from __future__ import annotations from typing import Any from fastmcp import FastMCP from .providers import ProviderError from .services import generation as generation_service from .services import providers as provider_service from .services import skill_doc mcp = FastMCP("ai-image") @mcp.tool def list_providers() -> list[dict[str, Any]]: """第 1 步:列出可用供应商及其已启用模型数。返回后必须调用 discover_models 拉取模型。""" out = [] for p in provider_service.list_providers(): if not p["enabled"]: continue out.append( { "provider": p["id"], "name": p["name"], "adapter": p["adapter"], "base_url": p["base_url"], "default_model": p["default_model"], "model_count": p["model_count"], "enabled_model_count": p["enabled_model_count"], "note": p["note"], } ) return out @mcp.tool def discover_models(provider_id: str) -> dict[str, Any]: """第 2 步(必做):拉取指定供应商的上游模型清单。 返回 {provider_id, total, matched}。matched 为模型 ID 转小写后包含 "image" 的集合, 是唯一可用的模型来源;matched 为空时必须停止并回复「没找到有效的模型」。 """ try: return provider_service.discover(provider_id, require_enabled=True) except ProviderError as exc: return {"provider_id": provider_id, "total": 0, "matched": [], "error": exc.message} @mcp.tool def generate_image( provider: str, model: str, prompt: str, size: str = "1024x1024", ratio: str = "", n: int = 1, images: list[str] | None = None, profile_name: str = "agent", ) -> dict[str, Any]: """第 3 步:出图。model 必须是 discover_models 返回的 matched 之一。 profile_name 是调用方的画廊标识,建议显式传入 Agent 名称(如 codex)。 服务端不会自动识别 Agent 身份;未传时使用通用画廊 agent。 """ try: result = generation_service.generate( provider_id=provider, model=model, prompt=prompt, size=size, ratio=ratio, n=n, images=images or [], profile_name=profile_name, ) except ProviderError as exc: return {"status": "failed", "error": exc.message} return {"status": "success", **result} @mcp.tool def list_generations(limit: int = 20) -> list[dict[str, Any]]: """查询最近的生成记录。""" return generation_service.list_generations()[:limit] @mcp.tool def get_skill_doc() -> str: """获取最新版 SKILL.md 原文(不含固定模型名)。""" return skill_doc.get_skill()["body"] def build_http_app(path: str = "/"): """把 MCP 挂到已有 FastAPI 应用上(同一端口)。 返回的 Starlette 应用自带 path 路径,由入口分发器转发请求。 """ return mcp.http_app(path=path)