-
Notifications
You must be signed in to change notification settings - Fork 780
add servers workflow subcommand #428
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
src/mcp_agent/cli/cloud/commands/servers/workflows/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| """Workflows subcommand for servers.""" | ||
|
|
||
| from .main import list_workflows_for_server | ||
|
|
||
| __all__ = [ | ||
| "list_workflows_for_server", | ||
| ] |
180 changes: 180 additions & 0 deletions
180
src/mcp_agent/cli/cloud/commands/servers/workflows/main.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| """Server workflows command implementation.""" | ||
|
|
||
| import json | ||
| from typing import Optional | ||
|
|
||
| import typer | ||
| import yaml | ||
| from rich.table import Table | ||
|
|
||
| from mcp_agent.cli.core.utils import run_async | ||
| from mcp_agent.cli.mcp_app.api_client import WorkflowExecutionStatus | ||
| from ...utils import ( | ||
| setup_authenticated_client, | ||
| validate_output_format, | ||
| handle_server_api_errors, | ||
| resolve_server, | ||
| ) | ||
| from mcp_agent.cli.utils.ux import console, print_info | ||
|
|
||
|
|
||
| @handle_server_api_errors | ||
| def list_workflows_for_server( | ||
| server_id_or_url: str = typer.Argument(..., help="Server ID, app config ID, or server URL to list workflows for"), | ||
| limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of results to return"), | ||
| status: Optional[str] = typer.Option(None, "--status", help="Filter by status: running|failed|timed_out|canceled|terminated|completed|continued"), | ||
| format: Optional[str] = typer.Option("text", "--format", help="Output format (text|json|yaml)"), | ||
| ) -> None: | ||
| """List workflows for an MCP Server. | ||
| Examples: | ||
| mcp-agent cloud servers workflows app_abc123 | ||
| mcp-agent cloud servers workflows https://server.example.com --status running | ||
| mcp-agent cloud servers workflows apcnf_xyz789 --limit 10 --format json | ||
| """ | ||
| validate_output_format(format) | ||
| client = setup_authenticated_client() | ||
|
|
||
| if server_id_or_url.startswith(('http://', 'https://')): | ||
| resolved_server = resolve_server(client, server_id_or_url) | ||
|
|
||
| if hasattr(resolved_server, 'appId'): | ||
| app_id_or_config_id = resolved_server.appId | ||
| elif hasattr(resolved_server, 'appConfigurationId'): | ||
| app_id_or_config_id = resolved_server.appConfigurationId | ||
| else: | ||
| raise ValueError(f"Could not extract app ID or config ID from server: {server_id_or_url}") | ||
| else: | ||
| app_id_or_config_id = server_id_or_url | ||
|
|
||
| max_results = limit or 100 | ||
|
|
||
| status_filter = None | ||
| if status: | ||
| status_map = { | ||
| "running": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING, | ||
| "failed": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED, | ||
| "timed_out": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT, | ||
| "timeout": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT, # alias | ||
| "canceled": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED, | ||
| "cancelled": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED, # alias | ||
| "terminated": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TERMINATED, | ||
| "completed": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_COMPLETED, | ||
| "continued": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW, | ||
| "continued_as_new": WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW, | ||
| } | ||
| status_filter = status_map.get(status.lower()) | ||
| if not status_filter: | ||
| valid_statuses = "running|failed|timed_out|timeout|canceled|cancelled|terminated|completed|continued|continued_as_new" | ||
| raise typer.BadParameter(f"Invalid status '{status}'. Valid options: {valid_statuses}") | ||
|
|
||
| async def list_workflows_async(): | ||
| return await client.list_workflows( | ||
| app_id_or_config_id=app_id_or_config_id, | ||
| max_results=max_results | ||
| ) | ||
|
|
||
| response = run_async(list_workflows_async()) | ||
| workflows = response.workflows or [] | ||
|
|
||
| if status_filter: | ||
| workflows = [w for w in workflows if w.execution_status == status_filter] | ||
|
|
||
| if format == "json": | ||
| _print_workflows_json(workflows) | ||
| elif format == "yaml": | ||
| _print_workflows_yaml(workflows) | ||
| else: | ||
| _print_workflows_text(workflows, status, server_id_or_url) | ||
graphite-app[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def _print_workflows_text(workflows, status_filter, server_id_or_url): | ||
| """Print workflows in text format.""" | ||
| server_name = server_id_or_url | ||
|
|
||
| console.print(f"\n[bold blue]📊 Workflows for Server: {server_name}[/bold blue]") | ||
|
|
||
| if not workflows: | ||
| print_info("No workflows found for this server.") | ||
| return | ||
|
|
||
| console.print(f"\nFound {len(workflows)} workflow(s):") | ||
|
|
||
| table = Table(show_header=True, header_style="bold blue") | ||
| table.add_column("Workflow ID", style="cyan", width=20) | ||
| table.add_column("Name", style="green", width=20) | ||
| table.add_column("Status", style="yellow", width=15) | ||
| table.add_column("Run ID", style="dim", width=15) | ||
| table.add_column("Created", style="dim", width=20) | ||
| table.add_column("Principal", style="dim", width=15) | ||
|
|
||
| for workflow in workflows: | ||
| status_display = _get_status_display(workflow.execution_status) | ||
| created_display = workflow.created_at.strftime('%Y-%m-%d %H:%M:%S') if workflow.created_at else "N/A" | ||
| run_id_display = _truncate_string(workflow.run_id or "N/A", 15) | ||
|
|
||
| table.add_row( | ||
| _truncate_string(workflow.workflow_id, 20), | ||
| _truncate_string(workflow.name, 20), | ||
| status_display, | ||
| run_id_display, | ||
| created_display, | ||
| _truncate_string(workflow.principal_id, 15), | ||
| ) | ||
|
|
||
| console.print(table) | ||
|
|
||
| if status_filter: | ||
| console.print(f"\n[dim]Filtered by status: {status_filter}[/dim]") | ||
graphite-app[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def _print_workflows_json(workflows): | ||
| """Print workflows in JSON format.""" | ||
| workflows_data = [_workflow_to_dict(workflow) for workflow in workflows] | ||
| print(json.dumps({"workflows": workflows_data}, indent=2, default=str)) | ||
|
|
||
|
|
||
| def _print_workflows_yaml(workflows): | ||
| """Print workflows in YAML format.""" | ||
| workflows_data = [_workflow_to_dict(workflow) for workflow in workflows] | ||
| print(yaml.dump({"workflows": workflows_data}, default_flow_style=False)) | ||
graphite-app[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def _workflow_to_dict(workflow): | ||
| """Convert WorkflowInfo to dictionary.""" | ||
| return { | ||
| "workflow_id": workflow.workflow_id, | ||
| "run_id": workflow.run_id, | ||
| "name": workflow.name, | ||
| "created_at": workflow.created_at.isoformat() if workflow.created_at else None, | ||
| "principal_id": workflow.principal_id, | ||
| "execution_status": workflow.execution_status.value if workflow.execution_status else None, | ||
| } | ||
|
|
||
|
|
||
| def _truncate_string(text: str, max_length: int) -> str: | ||
| """Truncate string to max_length, adding ellipsis if truncated.""" | ||
| if len(text) <= max_length: | ||
| return text | ||
| return text[:max_length-3] + "..." | ||
|
|
||
|
|
||
| def _get_status_display(status): | ||
| """Convert WorkflowExecutionStatus to display string with emoji.""" | ||
| if not status: | ||
| return "❓ Unknown" | ||
|
|
||
| status_map = { | ||
| WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING: "[green]🟢 Running[/green]", | ||
| WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_COMPLETED: "[blue]✅ Completed[/blue]", | ||
| WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED: "[red]❌ Failed[/red]", | ||
| WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED: "[yellow]🟡 Canceled[/yellow]", | ||
| WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TERMINATED: "[red]🔴 Terminated[/red]", | ||
| WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT: "[orange]⏰ Timed Out[/orange]", | ||
| WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW: "[purple]🔄 Continued[/purple]", | ||
| } | ||
|
|
||
| return status_map.get(status, "❓ Unknown") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The status filtering is currently applied client-side after fetching up to
max_resultsworkflows from the API. This approach may miss relevant workflows if there are more matching workflows than the limit allows.Consider modifying the API client to pass the status filter directly to the API endpoint, which would:
If the API doesn't support status filtering, consider increasing the
max_resultsvalue when a status filter is applied to reduce the chance of missing relevant workflows.Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed this to track: https://linear.app/lastmile-ai/issue/LAS-2032/list-workflow-filtering-may-miss-results-due-to-client-side-filtering