Files
ewoooc/services/market_intel/mcp_contract.py
OoO 07b76870c9
All checks were successful
CD Pipeline / deploy (push) Successful in 1m1s
feat(market-intel): add internal mcp contract
2026-05-18 14:42:25 +08:00

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,
}