129 lines
4.3 KiB
Python
129 lines
4.3 KiB
Python
"""市場情報內部 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,
|
|
}
|