Skip to content

Commit fe2567c

Browse files
authored
Create retry utility and mark certain errors as non-retriable (#476)
### TL;DR Added retry mechanism for CLI commands with proper error handling for non-retriable errors. ### What changed? - Added a new `retriable` flag to `CLIError` class to indicate whether an error can be retried - Set authentication and validation errors as non-retriable by adding `retriable=False` to relevant error messages - Created a new retry utility module (`src/mcp_agent/cli/utils/retry.py`) with: - Functions to handle exponential backoff retries for both synchronous and asynchronous operations - Logic to determine if errors are retriable based on error type and the new `retriable` flag - A `RetryError` class to provide context about retry failures ### How to test? 1. Test CLI commands that previously failed with authentication errors to ensure they still fail appropriately 2. Verify that validation errors (like invalid API keys or formats) fail immediately without retries 3. Test network-related operations to confirm they properly retry on transient failures 4. Check that the exponential backoff works as expected with increasing delays between retry attempts <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Adds automatic retry utilities with exponential backoff for CLI operations to improve reliability. - Bug Fixes - Marks authentication- and parsing-related failures as non-retriable to avoid futile retries (login, whoami, workflows, secrets, and related commands). - Ensures unauthenticated and expired-token errors are handled consistently across commands. - Chores - Standardizes error metadata to include an explicit retriable flag without changing messages or prompts. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 25909e8 commit fe2567c

File tree

12 files changed

+177
-20
lines changed

12 files changed

+177
-20
lines changed

src/mcp_agent/cli/cloud/commands/app/status/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ def get_app_status(
5757

5858
if not effective_api_key:
5959
raise CLIError(
60-
"Must be logged in to get app status. Run 'mcp-agent login', set MCP_API_KEY environment variable or specify --api-key option."
60+
"Must be logged in to get app status. Run 'mcp-agent login', set MCP_API_KEY environment variable or specify --api-key option.",
61+
retriable=False
6162
)
6263

6364
client = MCPAppClient(
@@ -92,7 +93,8 @@ def get_app_status(
9293

9394
except UnauthenticatedError as e:
9495
raise CLIError(
95-
"Invalid API key. Run 'mcp-agent login' or set MCP_API_KEY environment variable with new API key."
96+
"Invalid API key. Run 'mcp-agent login' or set MCP_API_KEY environment variable with new API key.",
97+
retriable=False
9698
) from e
9799
except Exception as e:
98100
# Re-raise with more context - top-level CLI handler will show clean message

src/mcp_agent/cli/cloud/commands/auth/login/main.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def login(
8888
if api_key:
8989
print_info("Using provided API key for authentication (MCP_API_KEY).")
9090
if not _is_valid_api_key(api_key):
91-
raise CLIError("Invalid API key provided.")
91+
raise CLIError("Invalid API key provided.", retriable=False)
9292

9393
credentials = _load_user_credentials(api_key)
9494

@@ -139,11 +139,11 @@ def _handle_manual_key_input() -> str:
139139

140140
if not input_api_key:
141141
print_warning("No API key provided.")
142-
raise CLIError("Failed to set valid API key")
142+
raise CLIError("Failed to set valid API key", retriable=False)
143143

144144
if not _is_valid_api_key(input_api_key):
145145
print_warning("Invalid API key provided.")
146-
raise CLIError("Failed to set valid API key")
146+
raise CLIError("Failed to set valid API key", retriable=False)
147147

148148
credentials = _load_user_credentials(input_api_key)
149149

src/mcp_agent/cli/cloud/commands/auth/whoami/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@ def whoami() -> None:
2929
)
3030
if not credentials:
3131
raise CLIError(
32-
"Not authenticated. Set MCP_API_KEY or run 'mcp-agent login'.", exit_code=4
32+
"Not authenticated. Set MCP_API_KEY or run 'mcp-agent login'.", exit_code=4, retriable=False
3333
)
3434

3535
if credentials.is_token_expired:
3636
raise CLIError(
3737
"Authentication token has expired. Use 'mcp-agent login' to re-authenticate.",
3838
exit_code=4,
39+
retriable=False,
3940
)
4041

4142
user_table = Table(show_header=False, box=None)

src/mcp_agent/cli/cloud/commands/utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ def setup_authenticated_client() -> MCPAppClient:
2929
effective_api_key = settings.API_KEY or load_api_key_credentials()
3030

3131
if not effective_api_key:
32-
raise CLIError(
33-
"Must be authenticated. Set MCP_API_KEY or run 'mcp-agent login'."
34-
)
32+
raise CLIError("Must be authenticated. Set MCP_API_KEY or run 'mcp-agent login'.", retriable=False)
3533

3634
return MCPAppClient(api_url=DEFAULT_API_BASE_URL, api_key=effective_api_key)
3735

@@ -48,7 +46,8 @@ def validate_output_format(format: str) -> None:
4846
valid_formats = ["text", "json", "yaml"]
4947
if format not in valid_formats:
5048
raise CLIError(
51-
f"Invalid format '{format}'. Valid options are: {', '.join(valid_formats)}"
49+
f"Invalid format '{format}'. Valid options are: {', '.join(valid_formats)}",
50+
retriable=False
5251
)
5352

5453

@@ -100,7 +99,8 @@ def wrapper(*args, **kwargs):
10099
return func(*args, **kwargs)
101100
except UnauthenticatedError as e:
102101
raise CLIError(
103-
"Invalid API key. Run 'mcp-agent login' or set MCP_API_KEY environment variable with new API key."
102+
"Invalid API key. Run 'mcp-agent login' or set MCP_API_KEY environment variable with new API key.",
103+
retriable=False
104104
) from e
105105
except CLIError:
106106
# Re-raise CLIErrors as-is

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ async def _cancel_workflow_async(
4141
effective_api_key = _settings.API_KEY or load_api_key_credentials()
4242

4343
if not effective_api_key:
44-
raise CLIError("Must be logged in to access server. Run 'mcp-agent login'.")
44+
raise CLIError("Must be logged in to access server. Run 'mcp-agent login'.", retriable=False)
4545

4646
try:
4747
async with mcp_connection_session(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ async def _describe_workflow_async(
4646
effective_api_key = _settings.API_KEY or load_api_key_credentials()
4747

4848
if not effective_api_key:
49-
raise CLIError("Must be logged in to access server. Run 'mcp-agent login'.")
49+
raise CLIError("Must be logged in to access server. Run 'mcp-agent login'.", retriable=False)
5050

5151
try:
5252
async with mcp_connection_session(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ async def _list_workflows_async(server_id_or_url: str, format: str = "text") ->
4343
effective_api_key = _settings.API_KEY or load_api_key_credentials()
4444

4545
if not effective_api_key:
46-
raise CLIError("Must be logged in to access server. Run 'mcp-agent login'.")
46+
raise CLIError("Must be logged in to access server. Run 'mcp-agent login'.", retriable=False)
4747

4848
try:
4949
async with mcp_connection_session(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ async def _signal_workflow_async(
4545
effective_api_key = _settings.API_KEY or load_api_key_credentials()
4646

4747
if not effective_api_key:
48-
raise CLIError("Must be logged in to access server. Run 'mcp-agent login'.")
48+
raise CLIError("Must be logged in to access server. Run 'mcp-agent login'.", retriable=False)
4949

5050
try:
5151
async with mcp_connection_session(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async def _list_workflow_runs_async(
4747
effective_api_key = _settings.API_KEY or load_api_key_credentials()
4848

4949
if not effective_api_key:
50-
raise CLIError("Must be logged in to access server. Run 'mcp-agent login'.")
50+
raise CLIError("Must be logged in to access server. Run 'mcp-agent login'.", retriable=False)
5151

5252
try:
5353
async with mcp_connection_session(

src/mcp_agent/cli/exceptions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
class CLIError(Exception):
55
"""Exception for expected CLI errors that should show clean user-facing messages."""
66

7-
def __init__(self, message: str, exit_code: int = 1):
7+
def __init__(self, message: str, exit_code: int = 1, retriable: bool = True):
88
super().__init__(message)
99
self.exit_code = exit_code
10+
self.retriable = retriable

0 commit comments

Comments
 (0)