From 7a8f8691046f2b93fb2c884a918a4ffcee2e0817 Mon Sep 17 00:00:00 2001 From: OG T Date: Wed, 25 Mar 2026 12:46:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(api):=20Phase=2013.2=20#81=20PostgreSQL=20?= =?UTF-8?q?MCP=20Tool=20=E6=95=B4=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 整合 Approval/Incident/Timeline 查詢到 MCP Bridge: - list_approvals: 列出授權請求 (可依狀態篩選) - get_approval: 取得單一授權詳情 - list_incidents: 列出 Incident (可依狀態篩選) - list_timeline: 列出最近時間軸事件 Co-Authored-By: Claude Opus 4.5 --- apps/api/src/plugins/mcp/mcp_bridge.py | 162 +++++++++++++++++++++++-- 1 file changed, 153 insertions(+), 9 deletions(-) diff --git a/apps/api/src/plugins/mcp/mcp_bridge.py b/apps/api/src/plugins/mcp/mcp_bridge.py index 665d9e21..5dc81668 100644 --- a/apps/api/src/plugins/mcp/mcp_bridge.py +++ b/apps/api/src/plugins/mcp/mcp_bridge.py @@ -367,15 +367,49 @@ class MCPBridge: ], "database": [ MCPTool( - name="query", - description="Execute SQL query", + name="list_approvals", + description="List approval requests with optional status filter", input_schema={ "type": "object", "properties": { - "sql": {"type": "string"}, - "params": {"type": "array"}, + "status": {"type": "string", "description": "Filter by status: pending, approved, rejected, expired"}, + "limit": {"type": "integer", "description": "Max results (default: 20)"}, + }, + }, + server_name=server.name, + ), + MCPTool( + name="get_approval", + description="Get a single approval by ID", + input_schema={ + "type": "object", + "properties": { + "approval_id": {"type": "string", "description": "Approval UUID"}, + }, + "required": ["approval_id"], + }, + server_name=server.name, + ), + MCPTool( + name="list_incidents", + description="List incidents with optional status filter", + input_schema={ + "type": "object", + "properties": { + "status": {"type": "string", "description": "Filter by status: active, resolved, escalated"}, + "limit": {"type": "integer", "description": "Max results (default: 20)"}, + }, + }, + server_name=server.name, + ), + MCPTool( + name="list_timeline", + description="List recent timeline events", + input_schema={ + "type": "object", + "properties": { + "limit": {"type": "integer", "description": "Max results (default: 50)"}, }, - "required": ["sql"], }, server_name=server.name, ), @@ -694,10 +728,120 @@ class MCPBridge: # Database: 查詢 incident/approval 歷史 (Phase 13.2 #81) # ============================================= elif server.name == "database": - if tool_name == "query": - # TODO: 整合真實 PostgreSQL 查詢 - logger.info(f"[TODO] Database query: {parameters.get('sql', '')[:50]}") - return {"rows": [], "affected": 0, "note": "Phase 13.2 #81 待實作"} + from uuid import UUID + + from src.models.approval import ApprovalStatus + from src.services.approval_db import ( + get_approval_service, + get_timeline_service, + ) + from src.services.incident_service import get_incident_service + + if tool_name == "list_approvals": + # 列出 Approval 請求 + approval_svc = get_approval_service() + status_str = parameters.get("status") + limit = parameters.get("limit", 20) + + status_filter = None + if status_str: + try: + status_filter = ApprovalStatus(status_str.lower()) + except ValueError: + return {"error": f"Invalid status: {status_str}. Valid: pending, approved, rejected, expired"} + + approvals = await approval_svc.get_all_approvals( + status=status_filter, + limit=limit, + ) + + return { + "count": len(approvals), + "approvals": [ + { + "id": str(a.id), + "action": a.action[:80] if a.action else "", + "status": a.status.value if hasattr(a.status, 'value') else str(a.status), + "risk_level": a.risk_level.value if hasattr(a.risk_level, 'value') else str(a.risk_level), + "signatures": f"{a.current_signatures}/{a.required_signatures}", + "created_at": a.created_at.isoformat() if a.created_at else None, + } + for a in approvals + ], + } + + elif tool_name == "get_approval": + # 取得單一 Approval + approval_id = parameters.get("approval_id") + if not approval_id: + return {"error": "Missing 'approval_id' parameter"} + + approval_svc = get_approval_service() + try: + approval = await approval_svc.get_approval_by_id(UUID(approval_id)) + except ValueError: + return {"error": f"Invalid UUID format: {approval_id}"} + + if not approval: + return {"error": f"Approval not found: {approval_id}"} + + return { + "id": str(approval.id), + "action": approval.action, + "description": approval.description, + "status": approval.status.value if hasattr(approval.status, 'value') else str(approval.status), + "risk_level": approval.risk_level.value if hasattr(approval.risk_level, 'value') else str(approval.risk_level), + "required_signatures": approval.required_signatures, + "current_signatures": approval.current_signatures, + "signatures": [ + {"signer": s.signer_name, "timestamp": s.timestamp.isoformat()} + for s in (approval.signatures or []) + ], + "created_at": approval.created_at.isoformat() if approval.created_at else None, + "resolved_at": approval.resolved_at.isoformat() if approval.resolved_at else None, + } + + elif tool_name == "list_incidents": + # 列出 Incidents + incident_svc = get_incident_service() + status_filter = parameters.get("status") + limit = parameters.get("limit", 20) + + incidents = await incident_svc.get_active_incidents() + + # 狀態過濾 + if status_filter: + incidents = [i for i in incidents if i.status.value == status_filter.lower()] + + incidents = incidents[:limit] + + return { + "count": len(incidents), + "incidents": [ + { + "id": str(i.id), + "title": i.title[:80] if i.title else "", + "severity": i.severity.value if hasattr(i.severity, 'value') else str(i.severity), + "status": i.status.value if hasattr(i.status, 'value') else str(i.status), + "source": i.source, + "created_at": i.created_at.isoformat() if i.created_at else None, + } + for i in incidents + ], + } + + elif tool_name == "list_timeline": + # 列出 Timeline 事件 + timeline_svc = get_timeline_service() + limit = parameters.get("limit", 50) + + events = await timeline_svc.get_events(limit=limit) + + return { + "count": len(events), + "events": events, + } + else: return {"error": f"Unknown database tool: {tool_name}"}