Files
ewoooc/services/market_intel/mcp_activation_runbook.py
OoO d990316d74
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
feat(market-intel): add mcp activation runbook
2026-05-18 15:25:44 +08:00

171 lines
7.2 KiB
Python
Raw Permalink 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 啟用 runbook preview。
只組裝人工操作順序、gate、command shape 與 fallback不執行 docker、SSH、
SQL、health check、env 寫入或 router 啟用。
"""
from services.market_intel.mcp_deploy_preflight import build_mcp_deploy_preflight_plan
from services.market_intel.mcp_readiness import build_mcp_readiness_plan
def _step(key, label, gate, status, command_preview=None, notes=None):
item = {
"key": key,
"label": label,
"gate": gate,
"status": status,
}
if command_preview:
item["command_preview"] = command_preview
if notes:
item["notes"] = notes
return item
def build_mcp_activation_runbook_preview(
*,
preflight=None,
readiness=None,
):
"""建立 MCP 啟用流程;預設只讀 planned不做 health / DB / docker。"""
preflight = preflight or build_mcp_deploy_preflight_plan()
readiness = readiness or build_mcp_readiness_plan(execute_requested=False)
required_env_ready = bool(preflight["checks"].get("required_env_vars_present"))
compose_ready = bool(
preflight["checks"].get("compose_file_present")
and preflight["checks"].get("all_expected_services_declared")
and preflight["checks"].get("all_expected_containers_declared")
and preflight["checks"].get("all_public_mcp_ports_localhost_only")
)
readonly_design_ready = bool(
preflight["checks"].get("postgres_readonly_role_required")
and preflight["checks"].get("postgres_allowed_tables_limited")
and preflight["checks"].get("filesystem_mounts_read_only")
)
resource_guard_ready = bool(
preflight["checks"].get("firecrawl_resource_guard_present")
and preflight["checks"].get("chrome_reaper_declared")
)
router_still_off = bool(preflight["checks"].get("router_flag_still_off_before_health"))
health_checked = bool(readiness["readiness_checks"].get("external_server_health_checked"))
all_health_passed = bool(readiness["readiness_checks"].get("external_servers_all_healthy"))
contract_ready = bool(readiness["readiness_checks"].get("market_intel_tool_contract_ready"))
stages = [
_step(
"configure_required_env",
"設定 MCP_POSTGRES_PASSWORD、TAVILY_API_KEY、EXA_API_KEY值不得寫入 repo",
"required_env_vars_present",
"ready" if required_env_ready else "blocked",
),
_step(
"create_mcp_readonly_role",
"人工確認 momo-db 內存在 mcp_readonly且只 GRANT 允許表 SELECT",
"postgres_readonly_role_required",
"ready" if readonly_design_ready else "blocked",
command_preview="psql \"$DATABASE_URL\" -v ON_ERROR_STOP=1 -f <reviewed_mcp_readonly_grants.sql>",
notes="不可 stop/rm/recreate momo-db只允許 operator 在 DB 內建立或修正 readonly role。",
),
_step(
"start_external_mcp_stack",
"在 188 主機啟動 docker-compose.mcp.yml 管理的 momo-mcp-* stack",
"preflight_ready_to_start_stack",
"ready" if preflight["ready_to_start_stack"] else "blocked",
command_preview=preflight.get("operator_command_preview"),
),
_step(
"run_mcp_health_smoke",
"確認 3001/3002/3003/3004 四個 localhost health endpoint 全部 200",
"external_servers_all_healthy",
"pass" if all_health_passed else "pending" if health_checked else "blocked",
command_preview="curl http://localhost:3001/health && curl http://localhost:3002/health && curl http://localhost:3003/health && curl http://localhost:3004/health",
),
_step(
"enable_mcp_router_flag",
"只有 health 全過且 operator 批准後,才可設定 MCP_ROUTER_ENABLED=true",
"router_enable_after_health",
"ready" if all_health_passed and router_still_off and contract_ready else "blocked",
command_preview="MCP_ROUTER_ENABLED=true docker compose up -d --no-deps --force-recreate momo-app",
),
_step(
"run_market_intel_mcp_smoke",
"啟用 router 後只跑 market_intel MCP read-only smoke不寫 market_*",
"market_intel_contract_ready",
"ready" if contract_ready and all_health_passed else "blocked",
command_preview="/api/market_intel/mcp_readiness?execute=true&timeout=3",
),
]
checks = {
"compose_ready": compose_ready,
"required_env_ready": required_env_ready,
"readonly_design_ready": readonly_design_ready,
"resource_guard_ready": resource_guard_ready,
"router_still_off_before_health": router_still_off,
"market_intel_contract_ready": contract_ready,
"health_checked": health_checked,
"all_health_passed": all_health_passed,
"no_activation_actions_executed": True,
"no_database_lifecycle_action_executed": True,
"no_database_write_executed": True,
}
blocked_reasons = [
key for key, passed in checks.items()
if not passed and key not in {
"health_checked",
"all_health_passed",
}
]
if not health_checked:
blocked_reasons.append("external_health_smoke_not_executed")
elif not all_health_passed:
blocked_reasons.append("external_health_smoke_not_passed")
return {
"mode": "mcp_activation_runbook_preview",
"ready_for_operator_activation": not blocked_reasons,
"checks": checks,
"blocked_reasons": blocked_reasons,
"stages": stages,
"required_env_vars": [
item["name"]
for item in preflight.get("env_statuses", [])
if item.get("required")
],
"post_start_health_targets": preflight.get("post_start_health_targets", []),
"fallback_plan": [
{
"key": "router_flag_kill_switch",
"label": "若 smoke 失敗,先把 MCP_ROUTER_ENABLED 關回 false只 recreate momo-app",
},
{
"key": "stop_mcp_stack_only",
"label": "外部 MCP stack 異常時,只停 docker-compose.mcp.yml 管理的 momo-mcp-* 容器",
},
{
"key": "preserve_momo_db",
"label": "不得 stop/rm/recreate momo-db必要時只撤回 mcp_readonly grants",
},
],
"safety_contract": {
"does_not_execute_docker": True,
"does_not_execute_ssh": True,
"does_not_write_env": True,
"does_not_create_db_role": True,
"does_not_run_health_check": True,
"does_not_enable_router": True,
"does_not_write_database": True,
"does_not_attach_scheduler": True,
},
"deployment_actions_executed": False,
"docker_command_executed": False,
"ssh_command_executed": False,
"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,
}