From 96f6d85be35916c0a8b0427b94204c86290a84a8 Mon Sep 17 00:00:00 2001 From: John Corbett <547858+jtcorbett@users.noreply.github.com> Date: Thu, 11 Sep 2025 18:07:40 +0000 Subject: [PATCH 1/2] allow logger to also take URLs --- .../cli/cloud/commands/logger/tail/main.py | 47 ++++++---- .../cli/cloud/commands/servers/delete/main.py | 3 +- .../cloud/commands/servers/describe/main.py | 4 +- src/mcp_agent/cli/cloud/commands/utils.py | 30 ------ .../cloud/commands/workflows/cancel/main.py | 3 +- .../cloud/commands/workflows/describe/main.py | 3 +- .../cli/cloud/commands/workflows/list/main.py | 3 +- .../cloud/commands/workflows/resume/main.py | 3 +- .../cli/cloud/commands/workflows/runs/main.py | 3 +- src/mcp_agent/cli/core/utils.py | 93 ------------------- 10 files changed, 38 insertions(+), 154 deletions(-) diff --git a/src/mcp_agent/cli/cloud/commands/logger/tail/main.py b/src/mcp_agent/cli/cloud/commands/logger/tail/main.py index 7d099d43e..61b4de85a 100644 --- a/src/mcp_agent/cli/cloud/commands/logger/tail/main.py +++ b/src/mcp_agent/cli/cloud/commands/logger/tail/main.py @@ -6,7 +6,7 @@ import signal import sys from datetime import datetime, timezone -from typing import Optional, Dict, Any, List +from typing import Optional, Dict, Any, List, Union from urllib.parse import urlparse import httpx @@ -19,8 +19,9 @@ from mcp_agent.cli.auth import load_credentials, UserCredentials from mcp_agent.cli.cloud.commands.utils import setup_authenticated_client from mcp_agent.cli.core.api_client import UnauthenticatedError -from mcp_agent.cli.core.utils import parse_app_identifier, resolve_server_url +from mcp_agent.cli.core.utils import run_async from mcp_agent.cli.utils.ux import print_error +from mcp_agent.cli.mcp_app.api_client import MCPApp, MCPAppConfiguration console = Console() @@ -29,7 +30,7 @@ def tail_logs( app_identifier: str = typer.Argument( - help="App ID or app configuration ID to retrieve logs for" + help="App ID, app configuration ID, or server URL to retrieve logs for" ), since: Optional[str] = typer.Option( None, @@ -91,6 +92,9 @@ def tail_logs( # Follow logs and filter for specific patterns mcp-agent cloud logger tail app_abc123 --follow --grep "authentication.*failed" + + # Use server URL instead of app ID + mcp-agent cloud logger tail https://abc123.mcpcloud.ai --follow """ credentials = load_credentials() @@ -130,14 +134,14 @@ def tail_logs( print_error("--format must be 'text', 'json', or 'yaml'") raise typer.Exit(6) - app_id, config_id = parse_app_identifier(app_identifier) - + client = setup_authenticated_client() + server = run_async(client.get_app_or_config(app_identifier)) + try: if follow: asyncio.run( _stream_logs( - app_id=app_id, - config_id=config_id, + server=server, credentials=credentials, grep_pattern=grep, app_identifier=app_identifier, @@ -147,9 +151,7 @@ def tail_logs( else: asyncio.run( _fetch_logs( - app_id=app_id, - config_id=config_id, - credentials=credentials, + server=server, since=since, grep_pattern=grep, limit=limit, @@ -157,6 +159,7 @@ def tail_logs( asc=asc, desc=desc, format=format, + app_identifier=app_identifier, ) ) @@ -168,9 +171,7 @@ def tail_logs( async def _fetch_logs( - app_id: Optional[str], - config_id: Optional[str], - credentials: UserCredentials, + server: Union[MCPApp, MCPAppConfiguration], since: Optional[str], grep_pattern: Optional[str], limit: int, @@ -178,9 +179,18 @@ async def _fetch_logs( asc: bool, desc: bool, format: str, + app_identifier: str, ) -> None: """Fetch logs one-time via HTTP API.""" + # Extract app_id and config_id from the server object + if hasattr(server, 'appId'): # MCPApp + app_id = server.appId + config_id = None + else: # MCPAppConfiguration + app_id = None + config_id = server.appConfigurationId + client = setup_authenticated_client() # Map order_by parameter from CLI to API format @@ -240,12 +250,11 @@ async def _fetch_logs( console.print("[yellow]No logs found matching the criteria[/yellow]") return - _display_logs(filtered_logs, title=f"Logs for {app_id or config_id}", format=format) + _display_logs(filtered_logs, title=f"Logs for {app_identifier}", format=format) async def _stream_logs( - app_id: Optional[str], - config_id: Optional[str], + server: Union[MCPApp, MCPAppConfiguration], credentials: UserCredentials, grep_pattern: Optional[str], app_identifier: str, @@ -253,7 +262,11 @@ async def _stream_logs( ) -> None: """Stream logs continuously via SSE.""" - server_url = await resolve_server_url(app_id, config_id, credentials) + # Get server URL directly from the server object + if not server.appServerInfo or not server.appServerInfo.serverUrl: + raise CLIError("Server URL not available - server may not be deployed") + + server_url = server.appServerInfo.serverUrl parsed = urlparse(server_url) stream_url = f"{parsed.scheme}://{parsed.netloc}/logs" diff --git a/src/mcp_agent/cli/cloud/commands/servers/delete/main.py b/src/mcp_agent/cli/cloud/commands/servers/delete/main.py index 9afcaf6ab..863a9aed1 100644 --- a/src/mcp_agent/cli/cloud/commands/servers/delete/main.py +++ b/src/mcp_agent/cli/cloud/commands/servers/delete/main.py @@ -6,7 +6,6 @@ from mcp_agent.cli.mcp_app.api_client import MCPApp from ...utils import ( setup_authenticated_client, - resolve_server, handle_server_api_errors, get_server_name, get_server_id, @@ -25,7 +24,7 @@ def delete_server( ) -> None: """Delete a specific MCP Server.""" client = setup_authenticated_client() - server = resolve_server(client, id_or_url) + server = run_async(client.get_app_or_config(id_or_url)) # Determine server type and delete function if isinstance(server, MCPApp): diff --git a/src/mcp_agent/cli/cloud/commands/servers/describe/main.py b/src/mcp_agent/cli/cloud/commands/servers/describe/main.py index 50ca0051c..13fd6729d 100644 --- a/src/mcp_agent/cli/cloud/commands/servers/describe/main.py +++ b/src/mcp_agent/cli/cloud/commands/servers/describe/main.py @@ -10,10 +10,10 @@ from ...utils import ( setup_authenticated_client, validate_output_format, - resolve_server, handle_server_api_errors, clean_server_status, ) +from mcp_agent.cli.core.utils import run_async from mcp_agent.cli.utils.ux import console @@ -29,7 +29,7 @@ def describe_server( """Describe a specific MCP Server.""" validate_output_format(format) client = setup_authenticated_client() - server = resolve_server(client, id_or_url) + server = run_async(client.get_app_or_config(id_or_url)) print_server_description(server, format) diff --git a/src/mcp_agent/cli/cloud/commands/utils.py b/src/mcp_agent/cli/cloud/commands/utils.py index 4cacb0391..4ee7969b3 100644 --- a/src/mcp_agent/cli/cloud/commands/utils.py +++ b/src/mcp_agent/cli/cloud/commands/utils.py @@ -6,7 +6,6 @@ from mcp_agent.cli.auth import load_api_key_credentials from mcp_agent.cli.core.api_client import UnauthenticatedError from mcp_agent.cli.core.constants import DEFAULT_API_BASE_URL -from mcp_agent.cli.core.utils import parse_app_identifier, run_async from mcp_agent.cli.exceptions import CLIError from mcp_agent.cli.mcp_app.api_client import ( MCPApp, @@ -48,35 +47,6 @@ def validate_output_format(format: str) -> None: ) -def resolve_server( - client: MCPAppClient, id_or_url: str -) -> Union[MCPApp, MCPAppConfiguration]: - """Resolve server from ID. - - Args: - client: Authenticated MCP App client - id_or_url: Server identifier (app ID or app config ID) - - Returns: - Server object (MCPApp or MCPAppConfiguration) - - Raises: - CLIError: If server resolution fails - """ - try: - app_id, config_id = parse_app_identifier(id_or_url) - - if config_id: - return run_async(client.get_app_configuration(app_config_id=config_id)) - else: - return run_async(client.get_app(app_id=app_id)) - - except ValueError as e: - raise CLIError(str(e)) from e - except Exception as e: - raise CLIError(f"Failed to resolve server '{id_or_url}': {str(e)}") from e - - def handle_server_api_errors(func): """Decorator to handle common API errors for server commands. diff --git a/src/mcp_agent/cli/cloud/commands/workflows/cancel/main.py b/src/mcp_agent/cli/cloud/commands/workflows/cancel/main.py index 880db9558..4cf7a3ca8 100644 --- a/src/mcp_agent/cli/cloud/commands/workflows/cancel/main.py +++ b/src/mcp_agent/cli/cloud/commands/workflows/cancel/main.py @@ -12,7 +12,6 @@ from mcp_agent.mcp.gen_client import gen_client from ...utils import ( setup_authenticated_client, - resolve_server, handle_server_api_errors, ) @@ -25,7 +24,7 @@ async def _cancel_workflow_async( server_url = server_id_or_url else: client = setup_authenticated_client() - server = resolve_server(client, server_id_or_url) + server = run_async(client.get_app_or_config(server_id_or_url)) if hasattr(server, "appServerInfo") and server.appServerInfo: server_url = server.appServerInfo.serverUrl diff --git a/src/mcp_agent/cli/cloud/commands/workflows/describe/main.py b/src/mcp_agent/cli/cloud/commands/workflows/describe/main.py index ee48783a7..2fe60f378 100644 --- a/src/mcp_agent/cli/cloud/commands/workflows/describe/main.py +++ b/src/mcp_agent/cli/cloud/commands/workflows/describe/main.py @@ -14,7 +14,6 @@ from mcp_agent.mcp.gen_client import gen_client from ...utils import ( setup_authenticated_client, - resolve_server, handle_server_api_errors, ) @@ -27,7 +26,7 @@ async def _describe_workflow_async( server_url = server_id_or_url else: client = setup_authenticated_client() - server = resolve_server(client, server_id_or_url) + server = run_async(client.get_app_or_config(server_id_or_url)) if hasattr(server, "appServerInfo") and server.appServerInfo: server_url = server.appServerInfo.serverUrl diff --git a/src/mcp_agent/cli/cloud/commands/workflows/list/main.py b/src/mcp_agent/cli/cloud/commands/workflows/list/main.py index f93be1a13..5c9871e50 100644 --- a/src/mcp_agent/cli/cloud/commands/workflows/list/main.py +++ b/src/mcp_agent/cli/cloud/commands/workflows/list/main.py @@ -15,7 +15,6 @@ from mcp_agent.mcp.gen_client import gen_client from ...utils import ( setup_authenticated_client, - resolve_server, handle_server_api_errors, validate_output_format, ) @@ -27,7 +26,7 @@ async def _list_workflows_async(server_id_or_url: str, format: str = "text") -> server_url = server_id_or_url else: client = setup_authenticated_client() - server = resolve_server(client, server_id_or_url) + server = run_async(client.get_app_or_config(server_id_or_url)) if hasattr(server, "appServerInfo") and server.appServerInfo: server_url = server.appServerInfo.serverUrl diff --git a/src/mcp_agent/cli/cloud/commands/workflows/resume/main.py b/src/mcp_agent/cli/cloud/commands/workflows/resume/main.py index e6771ec68..59e40e9b9 100644 --- a/src/mcp_agent/cli/cloud/commands/workflows/resume/main.py +++ b/src/mcp_agent/cli/cloud/commands/workflows/resume/main.py @@ -13,7 +13,6 @@ from mcp_agent.mcp.gen_client import gen_client from ...utils import ( setup_authenticated_client, - resolve_server, handle_server_api_errors, ) @@ -29,7 +28,7 @@ async def _signal_workflow_async( server_url = server_id_or_url else: client = setup_authenticated_client() - server = resolve_server(client, server_id_or_url) + server = run_async(client.get_app_or_config(server_id_or_url)) if hasattr(server, "appServerInfo") and server.appServerInfo: server_url = server.appServerInfo.serverUrl diff --git a/src/mcp_agent/cli/cloud/commands/workflows/runs/main.py b/src/mcp_agent/cli/cloud/commands/workflows/runs/main.py index 3bb4a6744..e1a6a46ea 100644 --- a/src/mcp_agent/cli/cloud/commands/workflows/runs/main.py +++ b/src/mcp_agent/cli/cloud/commands/workflows/runs/main.py @@ -14,7 +14,6 @@ from ...utils import ( setup_authenticated_client, validate_output_format, - resolve_server, ) from mcp_agent.cli.utils.ux import console, print_info @@ -27,7 +26,7 @@ async def _list_workflow_runs_async( server_url = server_id_or_url else: client = setup_authenticated_client() - server = resolve_server(client, server_id_or_url) + server = run_async(client.get_app_or_config(server_id_or_url)) if hasattr(server, "appServerInfo") and server.appServerInfo: server_url = server.appServerInfo.serverUrl diff --git a/src/mcp_agent/cli/core/utils.py b/src/mcp_agent/cli/core/utils.py index a35258d6d..3b50de292 100644 --- a/src/mcp_agent/cli/core/utils.py +++ b/src/mcp_agent/cli/core/utils.py @@ -113,96 +113,3 @@ def attach_stdio_servers( args=desc.get("args", []), ) app.context.config.mcp.servers[name] = settings - - -def parse_app_identifier(identifier: str) -> Tuple[Optional[str], Optional[str]]: - """Parse app identifier to extract app ID and config ID. - - Args: - identifier: App identifier (must be app_... or apcnf_...) - - Returns: - Tuple of (app_id, config_id) - - Raises: - ValueError: If identifier format is not recognized - """ - - if identifier.startswith("apcnf_"): - return None, identifier - - if identifier.startswith("app_"): - return identifier, None - - raise ValueError( - f"Invalid identifier format: '{identifier}'. Must be an app ID (app_...) or app configuration ID (apcnf_...)" - ) - - -async def resolve_server_url( - app_id: Optional[str], - config_id: Optional[str], - credentials: UserCredentials, -) -> str: - """Resolve server URL from app ID or configuration ID.""" - - if not app_id and not config_id: - raise CLIError("Either app_id or config_id must be provided") - - if app_id: - endpoint = "/mcp_app/get_app" - payload = {"app_id": app_id} - response_key = "app" - not_found_msg = f"App '{app_id}' not found" - not_deployed_msg = f"App '{app_id}' is not deployed yet" - no_url_msg = f"No server URL found for app '{app_id}'" - offline_msg = f"App '{app_id}' server is offline" - api_error_msg = "Failed to get app info" - else: - endpoint = "/mcp_app/get_app_configuration" - payload = {"app_configuration_id": config_id} - response_key = "appConfiguration" - not_found_msg = f"App configuration '{config_id}' not found" - not_deployed_msg = f"App configuration '{config_id}' is not deployed yet" - no_url_msg = f"No server URL found for app configuration '{config_id}'" - offline_msg = f"App configuration '{config_id}' server is offline" - api_error_msg = "Failed to get app configuration" - - api_base = DEFAULT_API_BASE_URL - headers = { - "Authorization": f"Bearer {credentials.api_key}", - "Content-Type": "application/json", - } - - try: - async with httpx.AsyncClient(timeout=30.0) as client: - response = await client.post( - f"{api_base}{endpoint}", json=payload, headers=headers - ) - - if response.status_code == 404: - raise CLIError(not_found_msg) - elif response.status_code != 200: - raise CLIError( - f"{api_error_msg}: {response.status_code} {response.text}" - ) - - data = response.json() - resource_info = data.get(response_key, {}) - server_info = resource_info.get("appServerInfo") - - if not server_info: - raise CLIError(not_deployed_msg) - - server_url = server_info.get("serverUrl") - if not server_url: - raise CLIError(no_url_msg) - - status = server_info.get("status", "APP_SERVER_STATUS_UNSPECIFIED") - if status == "APP_SERVER_STATUS_OFFLINE": - raise CLIError(offline_msg) - - return server_url - - except httpx.RequestError as e: - raise CLIError(f"Failed to connect to API: {e}") From 4b9d901bb662fc339f47c0eb3c71d30be229da47 Mon Sep 17 00:00:00 2001 From: John Corbett <547858+jtcorbett@users.noreply.github.com> Date: Thu, 11 Sep 2025 18:10:10 +0000 Subject: [PATCH 2/2] format --- src/mcp_agent/cli/core/utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/mcp_agent/cli/core/utils.py b/src/mcp_agent/cli/core/utils.py index 3b50de292..3a6b570fe 100644 --- a/src/mcp_agent/cli/core/utils.py +++ b/src/mcp_agent/cli/core/utils.py @@ -1,15 +1,11 @@ import asyncio import importlib.util -import httpx import sys from pathlib import Path -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict from mcp_agent.app import MCPApp -from mcp_agent.cli.exceptions import CLIError -from mcp_agent.cli.auth import UserCredentials -from mcp_agent.cli.core.constants import DEFAULT_API_BASE_URL from mcp_agent.config import MCPServerSettings, MCPSettings