Files
ewoooc/services/market_intel/mcp_deploy_preflight.py
OoO 6f68178959
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s
feat(market-intel): add external mcp preflight
2026-05-18 14:51:47 +08:00

179 lines
6.1 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 stack 部署前檢查 preview。
本模組只讀本機 compose 設計與環境變數狀態;不執行 docker、SSH、DB migration、
RBAC 建立或任何 MCP server health call。
"""
import os
from pathlib import Path
MCP_COMPOSE_PATH = "docker-compose.mcp.yml"
EXPECTED_MCP_SERVICES = (
"postgres-mcp",
"mcp-omnisearch",
"firecrawl-self",
"firecrawl-redis",
"firecrawl-playwright",
"chrome-reaper",
"filesystem-mcp",
)
EXPECTED_MCP_CONTAINERS = (
"momo-mcp-postgres",
"momo-mcp-omnisearch",
"momo-mcp-firecrawl",
"momo-mcp-firecrawl-redis",
"momo-mcp-firecrawl-playwright",
"momo-mcp-chrome-reaper",
"momo-mcp-filesystem",
)
EXPECTED_LOCALHOST_PORTS = ("3001", "3002", "3003", "3004")
REQUIRED_ENV_VARS = ("MCP_POSTGRES_PASSWORD", "TAVILY_API_KEY", "EXA_API_KEY")
OPTIONAL_ENV_VARS = ("FIRECRAWL_AUTH_KEY",)
def _read_compose_file(path):
compose_path = Path(path)
if not compose_path.exists():
return compose_path, ""
return compose_path, compose_path.read_text(encoding="utf-8")
def _build_env_status(env):
statuses = []
for name in REQUIRED_ENV_VARS:
statuses.append(
{
"name": name,
"required": True,
"present": bool(env.get(name)),
"value_redacted": bool(env.get(name)),
}
)
for name in OPTIONAL_ENV_VARS:
statuses.append(
{
"name": name,
"required": False,
"present": bool(env.get(name)),
"value_redacted": bool(env.get(name)),
}
)
return statuses
def build_mcp_deploy_preflight_plan(*, compose_path=MCP_COMPOSE_PATH, env=None):
"""建立外部 MCP 部署 preflight不執行任何部署動作。"""
env = env or os.environ
compose_file, compose_text = _read_compose_file(compose_path)
service_statuses = [
{
"service": service,
"declared": f" {service}:" in compose_text,
}
for service in EXPECTED_MCP_SERVICES
]
container_statuses = [
{
"container": container,
"declared": f"container_name: {container}" in compose_text,
}
for container in EXPECTED_MCP_CONTAINERS
]
port_statuses = [
{
"port": port,
"localhost_only": f"127.0.0.1:{port}:" in compose_text,
"declared": f":{port}" in compose_text,
}
for port in EXPECTED_LOCALHOST_PORTS
]
env_statuses = _build_env_status(env)
env_required_present = all(
item["present"]
for item in env_statuses
if item["required"]
)
router_enabled = env.get("MCP_ROUTER_ENABLED", "false").strip().lower() in (
"true",
"1",
"yes",
"on",
)
checks = {
"compose_file_present": compose_file.exists(),
"all_expected_services_declared": all(item["declared"] for item in service_statuses),
"all_expected_containers_declared": all(item["declared"] for item in container_statuses),
"all_public_mcp_ports_localhost_only": all(
item["localhost_only"] for item in port_statuses
),
"required_env_vars_present": env_required_present,
"postgres_readonly_role_required": "POSTGRES_USER=mcp_readonly" in compose_text,
"postgres_allowed_tables_limited": "ALLOWED_TABLES=" in compose_text,
"filesystem_mounts_read_only": "./data:/data:ro" in compose_text and "./logs:/logs:ro" in compose_text,
"firecrawl_resource_guard_present": "memory: 2g" in compose_text and "PLAYWRIGHT_BROWSER_POOL_MAX=3" in compose_text,
"chrome_reaper_declared": "chrome-reaper:" in compose_text,
"router_flag_still_off_before_health": not router_enabled,
"no_deployment_action_executed": True,
"no_database_lifecycle_action_executed": True,
}
blocked_reasons = [
key for key, passed in checks.items()
if not passed
]
return {
"mode": "mcp_external_deploy_preflight_preview",
"compose_path": str(compose_file),
"compose_file_present": compose_file.exists(),
"expected_services": list(EXPECTED_MCP_SERVICES),
"service_statuses": service_statuses,
"container_statuses": container_statuses,
"port_statuses": port_statuses,
"env_statuses": env_statuses,
"checks": checks,
"ready_to_start_stack": not blocked_reasons,
"blocked_reasons": blocked_reasons,
"operator_command_preview": (
"docker compose -f docker-compose.mcp.yml up -d"
if compose_file.exists()
else None
),
"post_start_health_targets": [
"http://localhost:3001/health",
"http://localhost:3002/health",
"http://localhost:3003/health",
"http://localhost:3004/health",
],
"fallback_plan": [
{
"key": "keep_router_disabled",
"label": "MCP_ROUTER_ENABLED 維持 false直到四個 health endpoint 全部 200",
},
{
"key": "stop_mcp_stack_only",
"label": "若 MCP stack 異常,只停止 docker-compose.mcp.yml 管理的 momo-mcp-* 容器",
},
{
"key": "no_momo_db_lifecycle_change",
"label": "不得 stop/rm/recreate momo-dbpostgres-mcp 只能使用 mcp_readonly",
},
],
"safe_deploy_boundaries": [
"do_not_use_remove_orphans",
"do_not_touch_momo_db_container",
"do_not_enable_mcp_router_before_health_passes",
"do_not_run_external_crawlers_without_operator_approval",
],
"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,
}