Skip to content

Commit 2642a52

Browse files
committed
add servers workflow subcommand
1 parent d0f0d96 commit 2642a52

File tree

9 files changed

+270
-7
lines changed

9 files changed

+270
-7
lines changed

src/mcp_agent/cli/cloud/commands/servers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
from .list.main import list_servers
44
from .describe.main import describe_server
55
from .delete.main import delete_server
6+
from .workflows.main import list_workflows_for_server
67

78
__all__ = [
89
"list_servers",
910
"describe_server",
1011
"delete_server",
12+
"list_workflows_for_server",
1113
]

src/mcp_agent/cli/cloud/commands/servers/list/main.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,13 @@ def list_servers(
2828
"""List MCP Servers with optional filtering and sorting.
2929
3030
Examples:
31-
# Filter servers containing 'api'
31+
3232
mcp-agent cloud servers list --filter api
3333
34-
# Sort by creation date (newest first)
3534
mcp-agent cloud servers list --sort-by -created
3635
37-
# Filter active servers and sort by name
3836
mcp-agent cloud servers list --filter active --sort-by name
3937
40-
# Get JSON output with filtering
4138
mcp-agent cloud servers list --filter production --format json
4239
"""
4340
validate_output_format(format)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Workflows subcommand for servers."""
2+
3+
from .main import list_workflows_for_server
4+
5+
__all__ = [
6+
"list_workflows_for_server",
7+
]
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
app_id_or_config_id: str = typer.Argument(..., help="App ID (app_xxx) or app config ID (apcnf_xxx) 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 apcnf_xyz789 --status running
35+
36+
mcp-agent cloud servers workflows app_abc123 --limit 10 --format json
37+
"""
38+
validate_output_format(format)
39+
client = setup_authenticated_client()
40+
41+
server = None
42+
try:
43+
server = resolve_server(client, app_id_or_config_id)
44+
except Exception:
45+
pass
46+
47+
max_results = limit or 100
48+
49+
status_filter = None
50+
if status:
51+
status_map = {
52+
"running": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING,
53+
"failed": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED,
54+
"timed_out": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT,
55+
"timeout": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT, # alias
56+
"canceled": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED,
57+
"cancelled": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED, # alias
58+
"terminated": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TERMINATED,
59+
"completed": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_COMPLETED,
60+
"continued": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW,
61+
"continued_as_new": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW,
62+
}
63+
status_filter = status_map.get(status.lower())
64+
if not status_filter:
65+
valid_statuses = "running|failed|timed_out|timeout|canceled|cancelled|terminated|completed|continued|continued_as_new"
66+
raise typer.BadParameter(f"Invalid status '{status}'. Valid options: {valid_statuses}")
67+
68+
async def list_workflows_async():
69+
return await client.list_workflows(
70+
app_id_or_config_id=app_id_or_config_id,
71+
max_results=max_results
72+
)
73+
74+
response = run_async(list_workflows_async())
75+
workflows = response.workflows or []
76+
77+
if status_filter:
78+
workflows = [w for w in workflows if w.execution_status == status_filter]
79+
80+
if format == "json":
81+
_print_workflows_json(workflows)
82+
elif format == "yaml":
83+
_print_workflows_yaml(workflows)
84+
else:
85+
_print_workflows_text(workflows, server, status, app_id_or_config_id)
86+
87+
88+
def _print_workflows_text(workflows, server, status_filter, app_id_or_config_id):
89+
"""Print workflows in text format."""
90+
if server and hasattr(server, 'name') and server.name:
91+
server_name = server.name
92+
else:
93+
server_name = app_id_or_config_id
94+
95+
console.print(f"\n[bold blue]📊 Workflows for Server: {server_name}[/bold blue]")
96+
97+
if not workflows:
98+
print_info("No workflows found for this server.")
99+
return
100+
101+
console.print(f"\nFound {len(workflows)} workflow(s):")
102+
103+
table = Table(show_header=True, header_style="bold blue")
104+
table.add_column("Workflow ID", style="cyan", width=20)
105+
table.add_column("Name", style="green", width=20)
106+
table.add_column("Status", style="yellow", width=15)
107+
table.add_column("Run ID", style="dim", width=15)
108+
table.add_column("Created", style="dim", width=20)
109+
table.add_column("Principal", style="dim", width=15)
110+
111+
for workflow in workflows:
112+
status_display = _get_status_display(workflow.execution_status)
113+
created_display = workflow.created_at.strftime('%Y-%m-%d %H:%M:%S') if workflow.created_at else "N/A"
114+
run_id_display = _truncate_string(workflow.run_id or "N/A", 15)
115+
116+
table.add_row(
117+
_truncate_string(workflow.workflow_id, 20),
118+
_truncate_string(workflow.name, 20),
119+
status_display,
120+
run_id_display,
121+
created_display,
122+
_truncate_string(workflow.principal_id, 15),
123+
)
124+
125+
console.print(table)
126+
127+
if status_filter:
128+
console.print(f"\n[dim]Filtered by status: {status_filter}[/dim]")
129+
130+
131+
def _print_workflows_json(workflows):
132+
"""Print workflows in JSON format."""
133+
workflows_data = [_workflow_to_dict(workflow) for workflow in workflows]
134+
print(json.dumps({"workflows": workflows_data}, indent=2, default=str))
135+
136+
137+
def _print_workflows_yaml(workflows):
138+
"""Print workflows in YAML format."""
139+
workflows_data = [_workflow_to_dict(workflow) for workflow in workflows]
140+
print(yaml.dump({"workflows": workflows_data}, default_flow_style=False))
141+
142+
143+
def _workflow_to_dict(workflow):
144+
"""Convert WorkflowInfo to dictionary."""
145+
return {
146+
"workflow_id": workflow.workflow_id,
147+
"run_id": workflow.run_id,
148+
"name": workflow.name,
149+
"created_at": workflow.created_at.isoformat() if workflow.created_at else None,
150+
"principal_id": workflow.principal_id,
151+
"execution_status": workflow.execution_status.value if workflow.execution_status else None,
152+
}
153+
154+
155+
def _truncate_string(text: str, max_length: int) -> str:
156+
"""Truncate string to max_length, adding ellipsis if truncated."""
157+
if len(text) <= max_length:
158+
return text
159+
return text[:max_length-3] + "..."
160+
161+
162+
def _get_status_display(status):
163+
"""Convert WorkflowExecutionStatus to display string with emoji."""
164+
if not status:
165+
return "❓ Unknown"
166+
167+
status_map = {
168+
WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING: "[green]🟢 Running[/green]",
169+
WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_COMPLETED: "[blue]✅ Completed[/blue]",
170+
WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED: "[red]❌ Failed[/red]",
171+
WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED: "[yellow]🟡 Canceled[/yellow]",
172+
WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TERMINATED: "[red]🔴 Terminated[/red]",
173+
WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT: "[orange]⏰ Timed Out[/orange]",
174+
WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW: "[purple]🔄 Continued[/purple]",
175+
}
176+
177+
return status_map.get(status, "❓ Unknown")

src/mcp_agent/cli/cloud/commands/workflows/cancel/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ def cancel_workflow(
8181
cannot be resumed and will be marked as cancelled.
8282
8383
Examples:
84+
8485
mcp-agent cloud workflows cancel app_abc123 run_xyz789
85-
mcp-agent cloud workflows cancel https://server.example.com run_xyz789 --reason "User requested cancellation"
86+
87+
mcp-agent cloud workflows cancel app_abc123 run_xyz789 --reason "User requested"
8688
"""
8789
run_async(_cancel_workflow_async(server_id_or_url, run_id, reason))

src/mcp_agent/cli/cloud/commands/workflows/describe/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,10 @@ def describe_workflow(
8686
creation time, and other metadata.
8787
8888
Examples:
89+
8990
mcp-agent cloud workflows describe app_abc123 run_xyz789
90-
mcp-agent cloud workflows describe https://server.example.com run_xyz789 --format json
91+
92+
mcp-agent cloud workflows describe app_abc123 run_xyz789 --format json
9193
"""
9294
if format not in ["text", "json", "yaml"]:
9395
console.print("[red]Error: --format must be 'text', 'json', or 'yaml'[/red]")

src/mcp_agent/cli/cloud/commands/workflows/resume/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,11 @@ def resume_workflow(
8686
a payload (JSON or text) to pass data to the resumed workflow.
8787
8888
Examples:
89+
8990
mcp-agent cloud workflows resume app_abc123 run_xyz789
90-
mcp-agent cloud workflows resume https://server.example.com run_xyz789 --payload '{"data": "value"}'
91+
92+
mcp-agent cloud workflows resume app_abc123 run_xyz789 --payload '{"data": "value"}'
93+
9194
mcp-agent cloud workflows resume app_abc123 run_xyz789 --payload "simple text"
9295
"""
9396
if payload:

src/mcp_agent/cli/cloud/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
list_servers,
3838
describe_server,
3939
delete_server,
40+
list_workflows_for_server,
4041
)
4142
from mcp_agent.cli.exceptions import CLIError
4243
from mcp_agent.cli.utils.ux import print_error
@@ -163,6 +164,7 @@ def invoke(self, ctx):
163164
app_cmd_servers.command(name="list")(list_servers)
164165
app_cmd_servers.command(name="describe")(describe_server)
165166
app_cmd_servers.command(name="delete")(delete_server)
167+
app_cmd_servers.command(name="workflows")(list_workflows_for_server)
166168
app.add_typer(app_cmd_servers, name="servers", help="Manage MCP Servers")
167169

168170
# Alias for servers - apps should behave identically

src/mcp_agent/cli/mcp_app/api_client.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import datetime
44
from typing import Any, Dict, List, Literal, Optional, Union
55
from urllib.parse import urlparse
6+
from enum import Enum
67

78
from pydantic import BaseModel
89

@@ -63,6 +64,35 @@ class CanDoActionsResponse(BaseModel):
6364
canDoActions: Optional[List[CanDoActionCheck]] = []
6465

6566

67+
class WorkflowExecutionStatus(Enum):
68+
"""From temporal.api.enums.v1.WorkflowExecutionStatus"""
69+
WORKFLOW_EXECUTION_STATUS_UNSPECIFIED = "WORKFLOW_EXECUTION_STATUS_UNSPECIFIED"
70+
WORKFLOW_EXECUTION_STATUS_RUNNING = "WORKFLOW_EXECUTION_STATUS_RUNNING"
71+
WORKFLOW_EXECUTION_STATUS_FAILED = "WORKFLOW_EXECUTION_STATUS_FAILED"
72+
WORKFLOW_EXECUTION_STATUS_TIMED_OUT = "WORKFLOW_EXECUTION_STATUS_TIMED_OUT"
73+
WORKFLOW_EXECUTION_STATUS_CANCELED = "WORKFLOW_EXECUTION_STATUS_CANCELED"
74+
WORKFLOW_EXECUTION_STATUS_TERMINATED = "WORKFLOW_EXECUTION_STATUS_TERMINATED"
75+
WORKFLOW_EXECUTION_STATUS_COMPLETED = "WORKFLOW_EXECUTION_STATUS_COMPLETED"
76+
WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW = "WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW"
77+
78+
79+
class WorkflowInfo(BaseModel):
80+
"""Information about a workflow execution instance"""
81+
workflow_id: str
82+
run_id: Optional[str] = None
83+
name: str
84+
created_at: datetime
85+
principal_id: str
86+
execution_status: Optional[WorkflowExecutionStatus] = None
87+
88+
89+
class ListWorkflowsResponse(BaseModel):
90+
"""Response for listing workflows"""
91+
workflows: Optional[List[WorkflowInfo]] = []
92+
next_page_token: Optional[str] = None
93+
total_count: Optional[int] = 0
94+
95+
6696
APP_ID_PREFIX = "app_"
6797
APP_CONFIG_ID_PREFIX = "apcnf_"
6898

@@ -464,6 +494,47 @@ async def list_app_configurations(
464494
response = await self.post("/mcp_app/list_app_configurations", payload)
465495
return ListAppConfigurationsResponse(**response.json())
466496

497+
async def list_workflows(
498+
self,
499+
app_id_or_config_id: str,
500+
name_filter: Optional[str] = None,
501+
max_results: int = 100,
502+
page_token: Optional[str] = None,
503+
) -> ListWorkflowsResponse:
504+
"""List workflows for a specific app or app configuration.
505+
506+
Args:
507+
app_id_or_config_id: The app ID (e.g. app_abc123) or app config ID (e.g. apcnf_xyz789)
508+
name_filter: Optional workflow name filter
509+
max_results: Maximum number of results to return
510+
page_token: Pagination token
511+
512+
Returns:
513+
ListWorkflowsResponse: The list of workflows
514+
515+
Raises:
516+
ValueError: If the app_id_or_config_id is invalid
517+
httpx.HTTPError: If the API request fails
518+
"""
519+
payload: Dict[str, Any] = {
520+
"max_results": max_results,
521+
}
522+
523+
if is_valid_app_id_format(app_id_or_config_id):
524+
payload["app_specifier"] = {"app_id": app_id_or_config_id}
525+
elif is_valid_app_config_id_format(app_id_or_config_id):
526+
payload["app_specifier"] = {"app_config_id": app_id_or_config_id}
527+
else:
528+
raise ValueError(f"Invalid app ID or app config ID format: {app_id_or_config_id}. Expected format: app_xxx or apcnf_xxx")
529+
530+
if name_filter:
531+
payload["name"] = name_filter
532+
if page_token:
533+
payload["page_token"] = page_token
534+
535+
response = await self.post("/workflow/list", payload)
536+
return ListWorkflowsResponse(**response.json())
537+
467538
async def delete_app(self, app_id: str) -> str:
468539
"""Delete an MCP App via the API.
469540

0 commit comments

Comments
 (0)