Skip to content

Commit e00d191

Browse files
committed
Add workflow commands to CLI
1 parent 130672d commit e00d191

File tree

11 files changed

+480
-5
lines changed

11 files changed

+480
-5
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""MCP Agent Cloud workflows commands."""
2+
3+
from .describe import describe_workflow
4+
from .suspend import suspend_workflow
5+
from .resume import resume_workflow
6+
from .cancel import cancel_workflow
7+
8+
__all__ = [
9+
"describe_workflow",
10+
"suspend_workflow",
11+
"resume_workflow",
12+
"cancel_workflow",
13+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""MCP Agent Cloud workflow cancel command."""
2+
3+
from .main import cancel_workflow
4+
5+
__all__ = ["cancel_workflow"]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Workflow cancel command implementation."""
2+
3+
from typing import Optional
4+
5+
import typer
6+
7+
from mcp_agent.cli.auth import load_api_key_credentials
8+
from mcp_agent.cli.core.api_client import UnauthenticatedError
9+
from mcp_agent.cli.core.constants import DEFAULT_API_BASE_URL
10+
from mcp_agent.cli.core.utils import run_async
11+
from mcp_agent.cli.exceptions import CLIError
12+
from mcp_agent.cli.utils.ux import console
13+
from mcp_agent.cli.workflows.api_client import WorkflowAPIClient
14+
15+
16+
def cancel_workflow(
17+
run_id: str = typer.Argument(..., help="Run ID of the workflow to cancel"),
18+
reason: Optional[str] = typer.Option(None, "--reason", help="Optional reason for cancellation"),
19+
) -> None:
20+
"""Cancel a workflow execution.
21+
22+
Permanently stops a workflow execution. Unlike suspend, a cancelled workflow
23+
cannot be resumed and will be marked as cancelled.
24+
25+
Examples:
26+
mcp-agent cloud workflows cancel run_abc123
27+
mcp-agent cloud workflows cancel run_abc123 --reason "User requested cancellation"
28+
"""
29+
effective_api_key = load_api_key_credentials()
30+
if not effective_api_key:
31+
raise CLIError(
32+
"Must be logged in to cancel workflow. Run 'mcp-agent login' or set MCP_API_KEY environment variable."
33+
)
34+
35+
client = WorkflowAPIClient(api_url=DEFAULT_API_BASE_URL, api_key=effective_api_key)
36+
37+
try:
38+
workflow_info = run_async(client.cancel_workflow(workflow_id=run_id, reason=reason))
39+
40+
console.print(f"[yellow]⚠[/yellow] Successfully cancelled workflow")
41+
console.print(f" Workflow ID: [cyan]{workflow_info.workflowId}[/cyan]")
42+
console.print(f" Run ID: [cyan]{workflow_info.runId or 'N/A'}[/cyan]")
43+
console.print(f" Status: [cyan]{_execution_status_text(workflow_info.executionStatus)}[/cyan]")
44+
45+
if reason:
46+
console.print(f" Reason: [dim]{reason}[/dim]")
47+
48+
except UnauthenticatedError as e:
49+
raise CLIError(
50+
"Authentication failed. Try running 'mcp-agent login'"
51+
) from e
52+
except Exception as e:
53+
raise CLIError(f"Error cancelling workflow with run ID {run_id}: {str(e)}") from e
54+
55+
56+
def _execution_status_text(status: str | None) -> str:
57+
"""Format the execution status text."""
58+
match status:
59+
case "WORKFLOW_EXECUTION_STATUS_RUNNING":
60+
return "🔄 Running"
61+
case "WORKFLOW_EXECUTION_STATUS_FAILED":
62+
return "❌ Failed"
63+
case "WORKFLOW_EXECUTION_STATUS_TIMED_OUT":
64+
return "⌛ Timed Out"
65+
case "WORKFLOW_EXECUTION_STATUS_CANCELED":
66+
return "🚫 Cancelled"
67+
case "WORKFLOW_EXECUTION_STATUS_TERMINATED":
68+
return "🛑 Terminated"
69+
case "WORKFLOW_EXECUTION_STATUS_COMPLETED":
70+
return "✅ Completed"
71+
case "WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW":
72+
return "🔁 Continued as New"
73+
case _:
74+
return "❓ Unknown"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""MCP Agent Cloud workflow describe command."""
2+
3+
from .main import describe_workflow
4+
5+
__all__ = ["describe_workflow"]
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Workflow describe command implementation."""
2+
3+
import json
4+
from typing import Optional
5+
6+
import typer
7+
import yaml
8+
from rich.panel import Panel
9+
10+
from mcp_agent.cli.auth import load_api_key_credentials
11+
from mcp_agent.cli.core.api_client import UnauthenticatedError
12+
from mcp_agent.cli.core.constants import DEFAULT_API_BASE_URL
13+
from mcp_agent.cli.core.utils import run_async
14+
from mcp_agent.cli.exceptions import CLIError
15+
from mcp_agent.cli.utils.ux import console
16+
from mcp_agent.cli.workflows.api_client import WorkflowAPIClient, WorkflowInfo
17+
18+
19+
def describe_workflow(
20+
run_id: str = typer.Argument(..., help="Run ID of the workflow to describe"),
21+
format: Optional[str] = typer.Option("text", "--format", help="Output format (text|json|yaml)"),
22+
) -> None:
23+
"""Describe a workflow execution (alias: status).
24+
25+
Shows detailed information about a workflow execution including its current status,
26+
creation time, and other metadata.
27+
"""
28+
# Validate format
29+
if format not in ["text", "json", "yaml"]:
30+
console.print("[red]Error: --format must be 'text', 'json', or 'yaml'[/red]")
31+
raise typer.Exit(6)
32+
33+
effective_api_key = load_api_key_credentials()
34+
if not effective_api_key:
35+
raise CLIError(
36+
"Must be logged in to describe workflow. Run 'mcp-agent login' or set MCP_API_KEY environment variable."
37+
)
38+
39+
client = WorkflowAPIClient(api_url=DEFAULT_API_BASE_URL, api_key=effective_api_key)
40+
41+
try:
42+
workflow_info = run_async(client.get_workflow(run_id))
43+
44+
if not workflow_info:
45+
raise CLIError(f"Workflow with run ID '{run_id}' not found.")
46+
47+
if format == "json":
48+
print(json.dumps(_workflow_to_dict(workflow_info), indent=2))
49+
elif format == "yaml":
50+
print(yaml.dump(_workflow_to_dict(workflow_info), default_flow_style=False))
51+
else: # text format
52+
print_workflow_info(workflow_info)
53+
54+
except UnauthenticatedError as e:
55+
raise CLIError(
56+
"Authentication failed. Try running 'mcp-agent login'"
57+
) from e
58+
except Exception as e:
59+
raise CLIError(f"Error describing workflow with run ID {run_id}: {str(e)}") from e
60+
61+
62+
def print_workflow_info(workflow_info: WorkflowInfo) -> None:
63+
"""Print workflow information in text format."""
64+
console.print(
65+
Panel(
66+
f"Name: [cyan]{workflow_info.name}[/cyan]\n"
67+
f"Workflow ID: [cyan]{workflow_info.workflowId}[/cyan]\n"
68+
f"Run ID: [cyan]{workflow_info.runId or 'N/A'}[/cyan]\n"
69+
f"Created: [cyan]{workflow_info.createdAt.strftime('%Y-%m-%d %H:%M:%S')}[/cyan]\n"
70+
f"Creator: [cyan]{workflow_info.principalId}[/cyan]\n"
71+
f"Status: [cyan]{_execution_status_text(workflow_info.executionStatus)}[/cyan]",
72+
title="Workflow",
73+
border_style="blue",
74+
expand=False,
75+
)
76+
)
77+
78+
79+
def _workflow_to_dict(workflow_info: WorkflowInfo) -> dict:
80+
"""Convert workflow info to dictionary for JSON/YAML output."""
81+
return {
82+
"name": workflow_info.name,
83+
"workflowId": workflow_info.workflowId,
84+
"runId": workflow_info.runId,
85+
"createdAt": workflow_info.createdAt.isoformat(),
86+
"creator": workflow_info.principalId,
87+
"executionStatus": workflow_info.executionStatus,
88+
"status": _execution_status_text(workflow_info.executionStatus),
89+
}
90+
91+
92+
def _execution_status_text(status: Optional[str]) -> str:
93+
"""Format the execution status text."""
94+
match status:
95+
case "WORKFLOW_EXECUTION_STATUS_RUNNING":
96+
return "🔄 Running"
97+
case "WORKFLOW_EXECUTION_STATUS_FAILED":
98+
return "❌ Failed"
99+
case "WORKFLOW_EXECUTION_STATUS_TIMED_OUT":
100+
return "⌛ Timed Out"
101+
case "WORKFLOW_EXECUTION_STATUS_CANCELED":
102+
return "🚫 Cancelled"
103+
case "WORKFLOW_EXECUTION_STATUS_TERMINATED":
104+
return "🛑 Terminated"
105+
case "WORKFLOW_EXECUTION_STATUS_COMPLETED":
106+
return "✅ Completed"
107+
case "WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW":
108+
return "🔁 Continued as New"
109+
case _:
110+
return "❓ Unknown"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""MCP Agent Cloud workflow resume command."""
2+
3+
from .main import resume_workflow
4+
5+
__all__ = ["resume_workflow"]
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Workflow resume command implementation."""
2+
3+
import json
4+
from typing import Optional
5+
6+
import typer
7+
8+
from mcp_agent.cli.auth import load_api_key_credentials
9+
from mcp_agent.cli.core.api_client import UnauthenticatedError
10+
from mcp_agent.cli.core.constants import DEFAULT_API_BASE_URL
11+
from mcp_agent.cli.core.utils import run_async
12+
from mcp_agent.cli.exceptions import CLIError
13+
from mcp_agent.cli.utils.ux import console
14+
from mcp_agent.cli.workflows.api_client import WorkflowAPIClient
15+
16+
17+
def resume_workflow(
18+
run_id: str = typer.Argument(..., help="Run ID of the workflow to resume"),
19+
payload: Optional[str] = typer.Option(None, "--payload", help="JSON or text payload to pass to resumed workflow"),
20+
) -> None:
21+
"""Resume a suspended workflow execution.
22+
23+
Resumes execution of a previously suspended workflow. Optionally accepts
24+
a payload (JSON or text) to pass data to the resumed workflow.
25+
26+
Examples:
27+
mcp-agent cloud workflows resume run_abc123
28+
mcp-agent cloud workflows resume run_abc123 --payload '{"data": "value"}'
29+
mcp-agent cloud workflows resume run_abc123 --payload "simple text"
30+
"""
31+
effective_api_key = load_api_key_credentials()
32+
if not effective_api_key:
33+
raise CLIError(
34+
"Must be logged in to resume workflow. Run 'mcp-agent login' or set MCP_API_KEY environment variable."
35+
)
36+
37+
# Validate payload if provided
38+
if payload:
39+
try:
40+
# Try to parse as JSON to validate format
41+
json.loads(payload)
42+
console.print(f"[dim]Resuming with JSON payload...[/dim]")
43+
except json.JSONDecodeError:
44+
# If not JSON, treat as plain text
45+
console.print(f"[dim]Resuming with text payload...[/dim]")
46+
47+
client = WorkflowAPIClient(api_url=DEFAULT_API_BASE_URL, api_key=effective_api_key)
48+
49+
try:
50+
workflow_info = run_async(client.resume_workflow(workflow_id=run_id, payload=payload))
51+
52+
console.print(f"[green]✓[/green] Successfully resumed workflow")
53+
console.print(f" Workflow ID: [cyan]{workflow_info.workflowId}[/cyan]")
54+
console.print(f" Run ID: [cyan]{workflow_info.runId or 'N/A'}[/cyan]")
55+
console.print(f" Status: [cyan]{_execution_status_text(workflow_info.executionStatus)}[/cyan]")
56+
57+
except UnauthenticatedError as e:
58+
raise CLIError(
59+
"Authentication failed. Try running 'mcp-agent login'"
60+
) from e
61+
except Exception as e:
62+
raise CLIError(f"Error resuming workflow with run ID {run_id}: {str(e)}") from e
63+
64+
65+
def _execution_status_text(status: str | None) -> str:
66+
"""Format the execution status text."""
67+
match status:
68+
case "WORKFLOW_EXECUTION_STATUS_RUNNING":
69+
return "🔄 Running"
70+
case "WORKFLOW_EXECUTION_STATUS_FAILED":
71+
return "❌ Failed"
72+
case "WORKFLOW_EXECUTION_STATUS_TIMED_OUT":
73+
return "⌛ Timed Out"
74+
case "WORKFLOW_EXECUTION_STATUS_CANCELED":
75+
return "🚫 Cancelled"
76+
case "WORKFLOW_EXECUTION_STATUS_TERMINATED":
77+
return "🛑 Terminated"
78+
case "WORKFLOW_EXECUTION_STATUS_COMPLETED":
79+
return "✅ Completed"
80+
case "WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW":
81+
return "🔁 Continued as New"
82+
case _:
83+
return "❓ Unknown"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""MCP Agent Cloud workflow suspend command."""
2+
3+
from .main import suspend_workflow
4+
5+
__all__ = ["suspend_workflow"]
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Workflow suspend command implementation."""
2+
3+
import typer
4+
5+
from mcp_agent.cli.auth import load_api_key_credentials
6+
from mcp_agent.cli.core.api_client import UnauthenticatedError
7+
from mcp_agent.cli.core.constants import DEFAULT_API_BASE_URL
8+
from mcp_agent.cli.core.utils import run_async
9+
from mcp_agent.cli.exceptions import CLIError
10+
from mcp_agent.cli.utils.ux import console
11+
from mcp_agent.cli.workflows.api_client import WorkflowAPIClient
12+
13+
14+
def suspend_workflow(
15+
run_id: str = typer.Argument(..., help="Run ID of the workflow to suspend"),
16+
) -> None:
17+
"""Suspend a workflow execution.
18+
19+
Pauses the execution of a running workflow. The workflow can be resumed later
20+
using the resume command.
21+
"""
22+
effective_api_key = load_api_key_credentials()
23+
if not effective_api_key:
24+
raise CLIError(
25+
"Must be logged in to suspend workflow. Run 'mcp-agent login' or set MCP_API_KEY environment variable."
26+
)
27+
28+
client = WorkflowAPIClient(api_url=DEFAULT_API_BASE_URL, api_key=effective_api_key)
29+
30+
try:
31+
# Note: Using run_id as workflow_id since the spec uses run-id as the argument
32+
# but the backend API expects workflow_id. This may need adjustment based on
33+
# how the backend interprets the workflow identification.
34+
workflow_info = run_async(client.suspend_workflow(workflow_id=run_id))
35+
36+
console.print(f"[green]✓[/green] Successfully suspended workflow")
37+
console.print(f" Workflow ID: [cyan]{workflow_info.workflowId}[/cyan]")
38+
console.print(f" Run ID: [cyan]{workflow_info.runId or 'N/A'}[/cyan]")
39+
console.print(f" Status: [cyan]{_execution_status_text(workflow_info.executionStatus)}[/cyan]")
40+
41+
except UnauthenticatedError as e:
42+
raise CLIError(
43+
"Authentication failed. Try running 'mcp-agent login'"
44+
) from e
45+
except Exception as e:
46+
raise CLIError(f"Error suspending workflow with run ID {run_id}: {str(e)}") from e
47+
48+
49+
def _execution_status_text(status: str | None) -> str:
50+
"""Format the execution status text."""
51+
match status:
52+
case "WORKFLOW_EXECUTION_STATUS_RUNNING":
53+
return "🔄 Running"
54+
case "WORKFLOW_EXECUTION_STATUS_FAILED":
55+
return "❌ Failed"
56+
case "WORKFLOW_EXECUTION_STATUS_TIMED_OUT":
57+
return "⌛ Timed Out"
58+
case "WORKFLOW_EXECUTION_STATUS_CANCELED":
59+
return "🚫 Cancelled"
60+
case "WORKFLOW_EXECUTION_STATUS_TERMINATED":
61+
return "🛑 Terminated"
62+
case "WORKFLOW_EXECUTION_STATUS_COMPLETED":
63+
return "✅ Completed"
64+
case "WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW":
65+
return "🔁 Continued as New"
66+
case _:
67+
return "❓ Unknown"

0 commit comments

Comments
 (0)