"""市場情報內部 MCP tool contract preview。 這裡只定義 market_intel 可使用的 read-only MCP 能力與白名單檢查; 不呼叫 MCP server、不寫 DB、不掛排程。 """ EXPECTED_MARKET_INTEL_TOOLS = ( "market_campaign_search", "market_campaign_scrape", "market_product_match_lookup", ) MARKET_INTEL_MCP_TOOL_CONTRACT = ( { "name": "market_campaign_search", "server": "omnisearch", "router_tools": ("tavily_search", "exa_search"), "purpose": "搜尋公開活動檔期入口與平台促銷資訊,僅產生候選來源。", "read_only": True, "database_write_allowed": False, "scheduler_attach_allowed": False, "external_network_required": True, }, { "name": "market_campaign_scrape", "server": "firecrawl", "router_tools": ("scrape_url",), "purpose": "抓取人工批准的公開活動頁內容,供 parser diagnostics 使用。", "read_only": True, "database_write_allowed": False, "scheduler_attach_allowed": False, "external_network_required": True, }, { "name": "market_product_match_lookup", "server": "postgres", "router_tools": ("query",), "purpose": "只讀查詢既有商品、比價與 market_* 表,輔助商品比對審核。", "read_only": True, "database_write_allowed": False, "scheduler_attach_allowed": False, "external_network_required": False, }, ) def get_market_intel_mcp_tool_contract(): """回傳可序列化的 market_intel MCP tool contract。""" return [ { **item, "router_tools": list(item["router_tools"]), } for item in MARKET_INTEL_MCP_TOOL_CONTRACT ] def build_mcp_tool_contract_preview(tool_registry=None): """檢查 market_intel MCP contract 是否已被 router 白名單覆蓋。""" if tool_registry is None: from services.mcp_router import TOOL_REGISTRY tool_registry = TOOL_REGISTRY registered = tool_registry.get("market_intel", {}) tool_statuses = [] for item in get_market_intel_mcp_tool_contract(): registered_tools = set(registered.get(item["server"], [])) expected_tools = set(item["router_tools"]) missing_tools = sorted(expected_tools - registered_tools) tool_statuses.append( { **item, "registered_router_tools": sorted(registered_tools), "missing_router_tools": missing_tools, "router_tools_whitelisted": not missing_tools, "write_risk": bool( not item["read_only"] or item["database_write_allowed"] or item["scheduler_attach_allowed"] ), } ) checks = { "market_intel_caller_registered": "market_intel" in tool_registry, "expected_tool_count_matched": len(tool_statuses) == len(EXPECTED_MARKET_INTEL_TOOLS), "all_expected_tool_names_present": { item["name"] for item in tool_statuses } == set(EXPECTED_MARKET_INTEL_TOOLS), "all_router_tools_whitelisted": all( item["router_tools_whitelisted"] for item in tool_statuses ), "all_tools_read_only": all(item["read_only"] for item in tool_statuses), "no_database_write_allowed": all( not item["database_write_allowed"] for item in tool_statuses ), "no_scheduler_attach_allowed": all( not item["scheduler_attach_allowed"] for item in tool_statuses ), } blocked_reasons = [ key for key, passed in checks.items() if not passed ] return { "mode": "mcp_tool_contract_preview", "caller": "market_intel", "expected_tool_names": list(EXPECTED_MARKET_INTEL_TOOLS), "tool_count": len(tool_statuses), "tools": tool_statuses, "checks": checks, "contract_ready": not blocked_reasons, "blocked_reasons": blocked_reasons, "database_session_created": False, "database_write_executed": False, "database_commit_executed": False, "external_network_executed": False, "scheduler_attached": False, "writes_executed": False, "would_write_database": False, }