diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py
index 602319c4..070e6d90 100644
--- a/apps/api/src/services/platform_operator_service.py
+++ b/apps/api/src/services/platform_operator_service.py
@@ -1151,6 +1151,67 @@ def _select_status_chain_source_id(
return incident_ids[0] if incident_ids else latest_incident_id or None
+def _status_chain_mcp_section(truth_chain: dict[str, Any] | None) -> dict[str, Any]:
+ mcp = truth_chain.get("mcp") if isinstance(truth_chain, dict) else {}
+ if not isinstance(mcp, dict):
+ mcp = {}
+ gateway = mcp.get("awooop_gateway") if isinstance(mcp.get("awooop_gateway"), dict) else {}
+ legacy = mcp.get("legacy") if isinstance(mcp.get("legacy"), dict) else {}
+
+ top_tools: list[dict[str, Any]] = []
+ seen_tools: set[str] = set()
+ for source, summary in (("gateway", gateway), ("legacy", legacy)):
+ by_tool = summary.get("by_tool") if isinstance(summary, dict) else []
+ if not isinstance(by_tool, list):
+ continue
+ for item in by_tool:
+ if not isinstance(item, dict):
+ continue
+ tool_name = str(item.get("tool_name") or "unknown").strip() or "unknown"
+ key = f"{source}:{tool_name}"
+ if key in seen_tools:
+ continue
+ seen_tools.add(key)
+ top_tools.append({
+ "source": source,
+ "tool_name": tool_name,
+ "total": (
+ _safe_int(item.get("total"))
+ or _safe_int(item.get("success"))
+ + _safe_int(item.get("failed"))
+ + _safe_int(item.get("blocked"))
+ ),
+ "success": _safe_int(item.get("success")),
+ "failed": _safe_int(item.get("failed")),
+ "blocked": _safe_int(item.get("blocked")),
+ "last_error": item.get("last_error"),
+ })
+ if len(top_tools) >= 5:
+ break
+ if len(top_tools) >= 5:
+ break
+
+ return {
+ "gateway": {
+ "total": _safe_int(gateway.get("total")),
+ "success": _safe_int(gateway.get("success")),
+ "failed": _safe_int(gateway.get("failed")),
+ "blocked": _safe_int(gateway.get("blocked")),
+ "first_class_total": _safe_int(gateway.get("first_class_total")),
+ "legacy_bridge_total": _safe_int(gateway.get("legacy_bridge_total")),
+ "policy_enforced_total": _safe_int(gateway.get("policy_enforced_total")),
+ "stage": gateway.get("stage"),
+ "stage_status": gateway.get("stage_status"),
+ },
+ "legacy": {
+ "total": _safe_int(legacy.get("total")),
+ "success": _safe_int(legacy.get("success")),
+ "failed": _safe_int(legacy.get("failed")),
+ },
+ "top_tools": top_tools,
+ }
+
+
def _build_awooop_status_chain(
*,
incident_ids: list[str],
@@ -1228,6 +1289,7 @@ def _build_awooop_status_chain(
):
needs_human = True
+ mcp_section = _status_chain_mcp_section(truth_chain)
blockers = [
str(item)
for item in [
@@ -1269,6 +1331,7 @@ def _build_awooop_status_chain(
"incident": latest.get("writes_incident_state"),
"auto_repair": latest.get("writes_auto_repair_result"),
},
+ "mcp": mcp_section,
}
diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py
index 7612bf6f..2c717411 100644
--- a/apps/api/tests/test_awooop_operator_timeline_labels.py
+++ b/apps/api/tests/test_awooop_operator_timeline_labels.py
@@ -470,6 +470,40 @@ def test_awooop_status_chain_marks_verified_repair() -> None:
},
"blockers": [],
},
+ "mcp": {
+ "awooop_gateway": {
+ "total": 2,
+ "success": 1,
+ "failed": 1,
+ "blocked": 0,
+ "first_class_total": 2,
+ "legacy_bridge_total": 0,
+ "policy_enforced_total": 2,
+ "stage": "provider_failed_after_gateway",
+ "stage_status": "failed",
+ "by_tool": [
+ {
+ "tool_name": "prometheus.query",
+ "total": 2,
+ "success": 1,
+ "failed": 1,
+ "blocked": 0,
+ }
+ ],
+ },
+ "legacy": {
+ "total": 1,
+ "success": 1,
+ "failed": 0,
+ "by_tool": [
+ {
+ "tool_name": "ssh_host",
+ "success": 1,
+ "failed": 0,
+ }
+ ],
+ },
+ },
},
remediation_history={
"total": 1,
@@ -492,6 +526,11 @@ def test_awooop_status_chain_marks_verified_repair() -> None:
assert chain["needs_human"] is False
assert chain["next_step"] == "monitor_for_regression"
assert chain["evidence"]["latest_route"] == "auto_repair_executor/rollout_restart/write"
+ assert chain["mcp"]["gateway"]["success"] == 1
+ assert chain["mcp"]["gateway"]["failed"] == 1
+ assert chain["mcp"]["gateway"]["policy_enforced_total"] == 2
+ assert chain["mcp"]["legacy"]["total"] == 1
+ assert chain["mcp"]["top_tools"][0]["tool_name"] == "prometheus.query"
def test_awooop_status_chain_marks_read_only_manual_gate() -> None:
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 172629f0..cb693616 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -431,6 +431,7 @@
"flowEvidenceOps": "Ops {count}",
"flowEvidenceKm": "KM {count}",
"flowEvidenceRepair": "Repair {count}",
+ "flowMcpDetail": "MCP detail: Gateway success {success} / failed {failed} / blocked {blocked}; first-class {firstClass}; legacy {legacy}; tools {tools}",
"flowTruthChainCurrent": "{stage} / {status}",
"flowComplete": "Complete",
"flowStages": {
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index 547ca9e6..44e11196 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -432,6 +432,7 @@
"flowEvidenceOps": "操作 {count}",
"flowEvidenceKm": "KM {count}",
"flowEvidenceRepair": "修復 {count}",
+ "flowMcpDetail": "MCP 明細:Gateway 成功 {success} / 失敗 {failed} / 阻擋 {blocked};一級治理 {firstClass};Legacy {legacy};工具 {tools}",
"flowTruthChainCurrent": "{stage} / {status}",
"flowComplete": "已完成",
"flowStages": {
diff --git a/apps/web/src/components/awooop/status-chain.tsx b/apps/web/src/components/awooop/status-chain.tsx
index cedf2c7d..c22a25ae 100644
--- a/apps/web/src/components/awooop/status-chain.tsx
+++ b/apps/web/src/components/awooop/status-chain.tsx
@@ -35,6 +35,33 @@ export interface AwoooPStatusChain {
incident?: boolean | null;
auto_repair?: boolean | null;
};
+ mcp?: {
+ gateway?: {
+ total?: number | null;
+ success?: number | null;
+ failed?: number | null;
+ blocked?: number | null;
+ first_class_total?: number | null;
+ legacy_bridge_total?: number | null;
+ policy_enforced_total?: number | null;
+ stage?: string | null;
+ stage_status?: string | null;
+ };
+ legacy?: {
+ total?: number | null;
+ success?: number | null;
+ failed?: number | null;
+ };
+ top_tools?: Array<{
+ source?: string | null;
+ tool_name?: string | null;
+ total?: number | null;
+ success?: number | null;
+ failed?: number | null;
+ blocked?: number | null;
+ last_error?: string | null;
+ }>;
+ };
}
function toneClass(chain?: AwoooPStatusChain | null) {
diff --git a/apps/web/src/components/incident/incident-card.tsx b/apps/web/src/components/incident/incident-card.tsx
index f9f9e26b..5ff2eac9 100644
--- a/apps/web/src/components/incident/incident-card.tsx
+++ b/apps/web/src/components/incident/incident-card.tsx
@@ -335,6 +335,22 @@ export function IncidentCard({ incident, decision, statusChain, onApprovalChange
active: (statusChain?.evidence?.auto_repair_records ?? 0) > 0,
},
] : []
+ const mcpTopTools = (statusChain?.mcp?.top_tools ?? []).slice(0, 3).map(tool => {
+ const toolName = chainValue(tool.tool_name)
+ return `${toolName} ${tool.success ?? 0}/${tool.failed ?? 0}/${tool.blocked ?? 0}`
+ }).join(', ')
+ const mcpGateway = statusChain?.mcp?.gateway
+ const mcpLegacy = statusChain?.mcp?.legacy
+ const statusChainMcpDetail = hasTruthChain
+ ? t('flowMcpDetail', {
+ success: mcpGateway?.success ?? 0,
+ failed: mcpGateway?.failed ?? 0,
+ blocked: mcpGateway?.blocked ?? 0,
+ firstClass: mcpGateway?.first_class_total ?? 0,
+ legacy: mcpLegacy?.total ?? 0,
+ tools: mcpTopTools || '--',
+ })
+ : null
const serviceName = incident.affected_services?.[0] ?? '--'
const duration = formatDuration(incident.created_at)
@@ -606,6 +622,10 @@ export function IncidentCard({ incident, decision, statusChain, onApprovalChange
))}
+ /
+
+ {statusChainMcpDetail}
+
>
)}