1+ """Server workflows command implementation."""
2+
3+ import json
4+ from typing import Optional
5+
6+ import typer
7+ import yaml
8+ from rich .table import Table
9+
10+ from mcp_agent .cli .core .utils import run_async
11+ from mcp_agent .cli .mcp_app .api_client import WorkflowExecutionStatus
12+ from ...utils import (
13+ setup_authenticated_client ,
14+ validate_output_format ,
15+ handle_server_api_errors ,
16+ resolve_server ,
17+ )
18+ from mcp_agent .cli .utils .ux import console , print_info
19+
20+
21+ @handle_server_api_errors
22+ def list_workflows_for_server (
23+ server_id_or_url : str = typer .Argument (..., help = "Server ID, app config ID, or server URL to list workflows for" ),
24+ limit : Optional [int ] = typer .Option (None , "--limit" , help = "Maximum number of results to return" ),
25+ status : Optional [str ] = typer .Option (None , "--status" , help = "Filter by status: running|failed|timed_out|canceled|terminated|completed|continued" ),
26+ format : Optional [str ] = typer .Option ("text" , "--format" , help = "Output format (text|json|yaml)" ),
27+ ) -> None :
28+ """List workflows for an MCP Server.
29+
30+ Examples:
31+
32+ mcp-agent cloud servers workflows app_abc123
33+
34+ mcp-agent cloud servers workflows https://server.example.com --status running
35+
36+ mcp-agent cloud servers workflows apcnf_xyz789 --limit 10 --format json
37+ """
38+ validate_output_format (format )
39+ client = setup_authenticated_client ()
40+
41+ if server_id_or_url .startswith (('http://' , 'https://' )):
42+ resolved_server = resolve_server (client , server_id_or_url )
43+
44+ if hasattr (resolved_server , 'appId' ):
45+ app_id_or_config_id = resolved_server .appId
46+ elif hasattr (resolved_server , 'appConfigurationId' ):
47+ app_id_or_config_id = resolved_server .appConfigurationId
48+ else :
49+ raise ValueError (f"Could not extract app ID or config ID from server: { server_id_or_url } " )
50+ else :
51+ app_id_or_config_id = server_id_or_url
52+
53+ max_results = limit or 100
54+
55+ status_filter = None
56+ if status :
57+ status_map = {
58+ "running" : WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_RUNNING ,
59+ "failed" : WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_FAILED ,
60+ "timed_out" : WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_TIMED_OUT ,
61+ "timeout" : WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_TIMED_OUT , # alias
62+ "canceled" : WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_CANCELED ,
63+ "cancelled" : WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_CANCELED , # alias
64+ "terminated" : WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_TERMINATED ,
65+ "completed" : WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_COMPLETED ,
66+ "continued" : WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW ,
67+ "continued_as_new" : WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW ,
68+ }
69+ status_filter = status_map .get (status .lower ())
70+ if not status_filter :
71+ valid_statuses = "running|failed|timed_out|timeout|canceled|cancelled|terminated|completed|continued|continued_as_new"
72+ raise typer .BadParameter (f"Invalid status '{ status } '. Valid options: { valid_statuses } " )
73+
74+ async def list_workflows_async ():
75+ return await client .list_workflows (
76+ app_id_or_config_id = app_id_or_config_id ,
77+ max_results = max_results
78+ )
79+
80+ response = run_async (list_workflows_async ())
81+ workflows = response .workflows or []
82+
83+ if status_filter :
84+ workflows = [w for w in workflows if w .execution_status == status_filter ]
85+
86+ if format == "json" :
87+ _print_workflows_json (workflows )
88+ elif format == "yaml" :
89+ _print_workflows_yaml (workflows )
90+ else :
91+ _print_workflows_text (workflows , status , server_id_or_url )
92+
93+
94+ def _print_workflows_text (workflows , status_filter , server_id_or_url ):
95+ """Print workflows in text format."""
96+ server_name = server_id_or_url
97+
98+ console .print (f"\n [bold blue]📊 Workflows for Server: { server_name } [/bold blue]" )
99+
100+ if not workflows :
101+ print_info ("No workflows found for this server." )
102+ return
103+
104+ console .print (f"\n Found { len (workflows )} workflow(s):" )
105+
106+ table = Table (show_header = True , header_style = "bold blue" )
107+ table .add_column ("Workflow ID" , style = "cyan" , width = 20 )
108+ table .add_column ("Name" , style = "green" , width = 20 )
109+ table .add_column ("Status" , style = "yellow" , width = 15 )
110+ table .add_column ("Run ID" , style = "dim" , width = 15 )
111+ table .add_column ("Created" , style = "dim" , width = 20 )
112+ table .add_column ("Principal" , style = "dim" , width = 15 )
113+
114+ for workflow in workflows :
115+ status_display = _get_status_display (workflow .execution_status )
116+ created_display = workflow .created_at .strftime ('%Y-%m-%d %H:%M:%S' ) if workflow .created_at else "N/A"
117+ run_id_display = _truncate_string (workflow .run_id or "N/A" , 15 )
118+
119+ table .add_row (
120+ _truncate_string (workflow .workflow_id , 20 ),
121+ _truncate_string (workflow .name , 20 ),
122+ status_display ,
123+ run_id_display ,
124+ created_display ,
125+ _truncate_string (workflow .principal_id , 15 ),
126+ )
127+
128+ console .print (table )
129+
130+ if status_filter :
131+ console .print (f"\n [dim]Filtered by status: { status_filter } [/dim]" )
132+
133+
134+ def _print_workflows_json (workflows ):
135+ """Print workflows in JSON format."""
136+ workflows_data = [_workflow_to_dict (workflow ) for workflow in workflows ]
137+ print (json .dumps ({"workflows" : workflows_data }, indent = 2 , default = str ))
138+
139+
140+ def _print_workflows_yaml (workflows ):
141+ """Print workflows in YAML format."""
142+ workflows_data = [_workflow_to_dict (workflow ) for workflow in workflows ]
143+ print (yaml .dump ({"workflows" : workflows_data }, default_flow_style = False ))
144+
145+
146+ def _workflow_to_dict (workflow ):
147+ """Convert WorkflowInfo to dictionary."""
148+ return {
149+ "workflow_id" : workflow .workflow_id ,
150+ "run_id" : workflow .run_id ,
151+ "name" : workflow .name ,
152+ "created_at" : workflow .created_at .isoformat () if workflow .created_at else None ,
153+ "principal_id" : workflow .principal_id ,
154+ "execution_status" : workflow .execution_status .value if workflow .execution_status else None ,
155+ }
156+
157+
158+ def _truncate_string (text : str , max_length : int ) -> str :
159+ """Truncate string to max_length, adding ellipsis if truncated."""
160+ if len (text ) <= max_length :
161+ return text
162+ return text [:max_length - 3 ] + "..."
163+
164+
165+ def _get_status_display (status ):
166+ """Convert WorkflowExecutionStatus to display string with emoji."""
167+ if not status :
168+ return "❓ Unknown"
169+
170+ status_map = {
171+ WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_RUNNING : "[green]🟢 Running[/green]" ,
172+ WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_COMPLETED : "[blue]✅ Completed[/blue]" ,
173+ WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_FAILED : "[red]❌ Failed[/red]" ,
174+ WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_CANCELED : "[yellow]🟡 Canceled[/yellow]" ,
175+ WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_TERMINATED : "[red]🔴 Terminated[/red]" ,
176+ WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_TIMED_OUT : "[orange]⏰ Timed Out[/orange]" ,
177+ WorkflowExecutionStatus .WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW : "[purple]🔄 Continued[/purple]" ,
178+ }
179+
180+ return status_map .get (status , "❓ Unknown" )
0 commit comments