diff --git a/src/mcp_agent/cli/__main__.py b/src/mcp_agent/cli/__main__.py index 09adcb09e..1295f56b8 100644 --- a/src/mcp_agent/cli/__main__.py +++ b/src/mcp_agent/cli/__main__.py @@ -25,35 +25,35 @@ } KNOWN = { - "go", - "check", - "chat", - "dev", - "invoke", - "serve", + # Curated top-level commands "init", "quickstart", "config", - "keys", - "models", - "server", - "build", - "logs", "doctor", - "configure", + "deploy", + "login", + "whoami", + "logout", "cloud", + # Umbrella group + "dev", } def main(): if len(sys.argv) > 1: first = sys.argv[1] - if first not in KNOWN: + # Back-compat: allow `mcp-agent go ...` + if first == "go": + sys.argv.insert(1, "dev") + elif first not in KNOWN: for i, arg in enumerate(sys.argv[1:], 1): if arg in GO_OPTIONS or any( arg.startswith(opt + "=") for opt in GO_OPTIONS ): - sys.argv.insert(i, "go") + # Route bare chat-like invocations to dev go (legacy behavior) + sys.argv.insert(i, "dev") + sys.argv.insert(i + 1, "go") break app() diff --git a/src/mcp_agent/cli/commands/chat.py b/src/mcp_agent/cli/commands/chat.py index 25d533b68..44c8db725 100644 --- a/src/mcp_agent/cli/commands/chat.py +++ b/src/mcp_agent/cli/commands/chat.py @@ -16,10 +16,13 @@ attach_stdio_servers, attach_url_servers, load_user_app, + detect_default_script, + select_servers_from_config, ) from mcp_agent.cli.utils.url_parser import generate_server_configs, parse_server_urls from mcp_agent.workflows.factory import create_llm from mcp_agent.agents.agent import Agent +from mcp_agent.config import get_settings app = typer.Typer(help="Ephemeral REPL for quick iteration") @@ -99,7 +102,7 @@ def chat( npx: Optional[str] = typer.Option(None, "--npx"), uvx: Optional[str] = typer.Option(None, "--uvx"), stdio: Optional[str] = typer.Option(None, "--stdio"), - script: Optional[Path] = typer.Option(Path("agent.py"), "--script"), + script: Optional[Path] = typer.Option(None, "--script"), list_servers: bool = typer.Option(False, "--list-servers"), list_tools: bool = typer.Option(False, "--list-tools"), list_resources: bool = typer.Option(False, "--list-resources"), @@ -107,6 +110,9 @@ def chat( None, "--server", help="Filter to a single server" ), ) -> None: + # Resolve script with auto-detection + script = detect_default_script(script) + server_list = servers_csv.split(",") if servers_csv else None url_servers = None @@ -140,12 +146,21 @@ def chat( else: server_list.extend(list(stdio_servers.keys())) + # Smart defaults for servers + resolved_server_list = select_servers_from_config( + servers_csv, url_servers, stdio_servers + ) + # Listing mode (no generation) if list_servers or list_tools or list_resources: try: async def _list(): - app_obj = load_user_app(script or Path("agent.py")) + # Disable progress display for cleaner listing output + settings = get_settings() + if settings.logger: + settings.logger.progress_display = False + app_obj = load_user_app(script, settings_override=settings) await app_obj.initialize() attach_url_servers(app_obj, url_servers) attach_stdio_servers(app_obj, stdio_servers) @@ -163,7 +178,7 @@ async def _list(): agent = Agent( name="chat-lister", instruction="You list tools and resources", - server_names=target_servers, + server_names=resolved_server_list or target_servers, context=app_obj.context, ) async with agent: @@ -203,7 +218,11 @@ async def _list(): ): async def _parallel_repl(): - app_obj = load_user_app(script or Path("agent.py")) + # Disable progress display for cleaner multi-model REPL + settings = get_settings() + if settings.logger: + settings.logger.progress_display = False + app_obj = load_user_app(script, settings_override=settings) await app_obj.initialize() attach_url_servers(app_obj, url_servers) attach_stdio_servers(app_obj, stdio_servers) @@ -227,7 +246,7 @@ async def _parallel_repl(): provider = prov_guess llm = create_llm( agent_name=m, - server_names=server_list or [], + server_names=resolved_server_list or [], provider=(provider or "openai"), model=m, context=app_obj.context, @@ -276,7 +295,9 @@ async def _parallel_repl(): ag = _Agent( name="chat-lister", instruction="list tools", - server_names=[srv] if srv else (server_list or []), + server_names=[srv] + if srv + else (resolved_server_list or []), context=app_obj.context, ) async with ag: @@ -294,7 +315,9 @@ async def _parallel_repl(): ag = _Agent( name="chat-lister", instruction="list resources", - server_names=[srv] if srv else (server_list or []), + server_names=[srv] + if srv + else (resolved_server_list or []), context=app_obj.context, ) async with ag: @@ -367,8 +390,8 @@ async def _gen(llm_instance): try: out = asyncio.run( _run_single_model( - script=script or Path("agent.py"), - servers=server_list, + script=script, + servers=resolved_server_list, url_servers=url_servers, stdio_servers=stdio_servers, model=m, @@ -392,9 +415,12 @@ async def _gen(llm_instance): and not models and not (list_servers or list_tools or list_resources) ): - # Interactive loop + # Interactive loop - disable progress display for cleaner REPL experience async def _repl(): - app_obj = load_user_app(script or Path("agent.py")) + settings = get_settings() + if settings.logger: + settings.logger.progress_display = False + app_obj = load_user_app(script, settings_override=settings) await app_obj.initialize() attach_url_servers(app_obj, url_servers) attach_stdio_servers(app_obj, stdio_servers) @@ -416,7 +442,7 @@ async def _repl(): provider = model_id.split(":", 1)[0] llm = create_llm( agent_name=(name or "chat"), - server_names=server_list or [], + server_names=resolved_server_list or [], provider=(provider or "openai"), model=model_id, context=app_obj.context, @@ -476,7 +502,7 @@ async def _repl(): # Recreate LLM with new model llm_local = create_llm( agent_name=(name or "chat"), - server_names=server_list or [], + server_names=resolved_server_list or [], provider=(prov or "openai"), model=model_id, context=app_obj.context, @@ -502,7 +528,9 @@ async def _repl(): ag = Agent( name="chat-lister", instruction="list tools", - server_names=[srv] if srv else (server_list or []), + server_names=[srv] + if srv + else (resolved_server_list or []), context=app_obj.context, ) async with ag: @@ -521,7 +549,9 @@ async def _repl(): ag = Agent( name="chat-lister", instruction="list resources", - server_names=[srv] if srv else (server_list or []), + server_names=[srv] + if srv + else (resolved_server_list or []), context=app_obj.context, ) async with ag: @@ -558,7 +588,7 @@ async def _repl(): prompt_msgs = await ag.create_prompt( prompt_name=prompt_name, arguments=arguments, - server_names=server_list or [], + server_names=resolved_server_list or [], ) # Generate with prompt messages out = await llm.generate_str(prompt_msgs) @@ -698,8 +728,8 @@ async def _repl(): else: out = asyncio.run( _run_single_model( - script=script or Path("agent.py"), - servers=server_list, + script=script, + servers=resolved_server_list, url_servers=url_servers, stdio_servers=stdio_servers, model=model, diff --git a/src/mcp_agent/cli/commands/dev.py b/src/mcp_agent/cli/commands/dev.py index d598893b5..89da14d16 100644 --- a/src/mcp_agent/cli/commands/dev.py +++ b/src/mcp_agent/cli/commands/dev.py @@ -15,6 +15,7 @@ from rich.console import Console from mcp_agent.config import get_settings +from mcp_agent.cli.core.utils import detect_default_script app = typer.Typer(help="Run app locally with diagnostics") @@ -22,7 +23,7 @@ @app.callback(invoke_without_command=True) -def dev(script: Path = typer.Option(Path("agent.py"), "--script")) -> None: +def dev(script: Path = typer.Option(None, "--script")) -> None: """Run the user's app script with optional live reload and preflight checks.""" def _preflight_ok() -> bool: @@ -49,6 +50,9 @@ def _run_script() -> subprocess.Popen: stdin=None, # Inherit stdin ) + # Resolve script path with auto-detection (main.py preferred) + script = detect_default_script(script) + # Simple preflight _ = _preflight_ok() diff --git a/src/mcp_agent/cli/commands/doctor.py b/src/mcp_agent/cli/commands/doctor.py index c593c342b..88bfde75c 100644 --- a/src/mcp_agent/cli/commands/doctor.py +++ b/src/mcp_agent/cli/commands/doctor.py @@ -412,7 +412,7 @@ def doctor() -> None: "• Add API key: [cyan]mcp-agent keys set [/cyan]\n" "• Add server: [cyan]mcp-agent server add recipe filesystem[/cyan]\n" "• Start chat: [cyan]mcp-agent chat --model anthropic.haiku[/cyan]\n" - "• Run agent: [cyan]mcp-agent dev --script agent.py[/cyan]", + "• Run agent: [cyan]mcp-agent dev start --script main.py[/cyan]", title="Getting Started", border_style="dim", ) diff --git a/src/mcp_agent/cli/commands/go.py b/src/mcp_agent/cli/commands/go.py index 2b763daf7..01304ad0f 100644 --- a/src/mcp_agent/cli/commands/go.py +++ b/src/mcp_agent/cli/commands/go.py @@ -18,6 +18,8 @@ attach_stdio_servers, attach_url_servers, load_user_app, + detect_default_script, + select_servers_from_config, ) from mcp_agent.cli.utils.url_parser import generate_server_configs, parse_server_urls from mcp_agent.workflows.factory import create_llm @@ -268,8 +270,11 @@ def go( npx: Optional[str] = typer.Option(None, "--npx"), uvx: Optional[str] = typer.Option(None, "--uvx"), stdio: Optional[str] = typer.Option(None, "--stdio"), - script: Optional[Path] = typer.Option(Path("agent.py"), "--script"), + script: Optional[Path] = typer.Option(None, "--script"), ) -> None: + # Resolve script with auto-detection + script = detect_default_script(script) + # Parse server names from config if provided server_list = servers.split(",") if servers else None @@ -302,6 +307,11 @@ def go( else: server_list.extend(list(stdio_servers.keys())) + # Smart defaults from config if still unspecified + resolved_server_list = select_servers_from_config( + ",".join(server_list) if server_list else None, url_servers, stdio_servers + ) + # Multi-model support if comma-separated if model and "," in model: models = [m.strip() for m in model.split(",") if m.strip()] @@ -311,7 +321,7 @@ def go( asyncio.run( _run_agent( app_script=script, - server_list=server_list, + server_list=resolved_server_list, model=m, message=message, prompt_file=prompt_file, @@ -331,7 +341,7 @@ def go( asyncio.run( _run_agent( app_script=script, - server_list=server_list, + server_list=resolved_server_list, model=model, message=message, prompt_file=prompt_file, diff --git a/src/mcp_agent/cli/commands/init.py b/src/mcp_agent/cli/commands/init.py index e0cce1d3f..a7bd07c7c 100644 --- a/src/mcp_agent/cli/commands/init.py +++ b/src/mcp_agent/cli/commands/init.py @@ -100,6 +100,7 @@ def init( console.print(f"Template: [cyan]{template}[/cyan] - {templates[template]}\n") files_created = [] + entry_script_name: str | None = None # Always create config files config_path = dir / "mcp_agent.config.yaml" @@ -122,15 +123,35 @@ def init( # Create template-specific files if template == "basic": - agent_path = dir / "agent.py" + # Determine entry script name and handle existing files + script_name = "main.py" + script_path = dir / script_name agent_content = _load_template("basic_agent.py") - if agent_content and _write(agent_path, agent_content, force): - files_created.append("agent.py") - # Make executable - try: - agent_path.chmod(agent_path.stat().st_mode | 0o111) - except Exception: - pass + + if agent_content: + write_force_flag = force + if script_path.exists() and not force: + if Confirm.ask(f"{script_path} exists. Overwrite?", default=False): + write_force_flag = True + else: + # Ask for an alternate filename and ensure it ends with .py + alt_name = Prompt.ask( + "Enter a filename to save the agent", default="agent.py" + ) + if not alt_name.endswith(".py"): + alt_name += ".py" + script_name = alt_name + script_path = dir / script_name + # keep write_force_flag as-is to allow overwrite prompt if needed + + if _write(script_path, agent_content, write_force_flag): + files_created.append(script_name) + entry_script_name = script_name + # Make executable + try: + script_path.chmod(script_path.stat().st_mode | 0o111) + except Exception: + pass elif template == "server": server_path = dir / "server.py" @@ -185,20 +206,29 @@ def init( console.print("2. Review and customize [cyan]mcp_agent.config.yaml[/cyan]") if template == "basic": - console.print("3. Run your agent: [cyan]python agent.py[/cyan]") - console.print(" Or use: [cyan]mcp-agent dev --script agent.py[/cyan]") - console.print(" Or chat: [cyan]mcp-agent chat[/cyan]") + run_file = entry_script_name or "main.py" + console.print(f"3. Run your agent: [cyan]uv run {run_file}[/cyan]") + console.print( + f" Or use: [cyan]mcp-agent dev start --script {run_file}[/cyan]" + ) + console.print( + f" Or serve: [cyan]mcp-agent dev serve --script {run_file}[/cyan]" + ) + console.print(" Or chat: [cyan]mcp-agent dev chat[/cyan]") + console.print( + "4. Edit config: [cyan]mcp-agent config edit[/cyan] (then rerun)" + ) elif template == "server": - console.print("3. Run the server: [cyan]python server.py[/cyan]") + console.print("3. Run the server: [cyan]uv run server.py[/cyan]") console.print( - " Or serve: [cyan]mcp-agent serve --script server.py[/cyan]" + " Or serve: [cyan]mcp-agent dev serve --script server.py[/cyan]" ) elif template == "token": - console.print("3. Run the example: [cyan]python token_example.py[/cyan]") + console.print("3. Run the example: [cyan]uv run token_example.py[/cyan]") console.print(" Watch token usage in real-time!") elif template == "factory": console.print("3. Customize agents in [cyan]agents.yaml[/cyan]") - console.print("4. Run the factory: [cyan]python factory.py[/cyan]") + console.print("4. Run the factory: [cyan]uv run factory.py[/cyan]") elif template == "minimal": console.print("3. Create your agent script") console.print(" See examples: [cyan]mcp-agent quickstart[/cyan]") diff --git a/src/mcp_agent/cli/commands/invoke.py b/src/mcp_agent/cli/commands/invoke.py index 38434f7ab..8864c423c 100644 --- a/src/mcp_agent/cli/commands/invoke.py +++ b/src/mcp_agent/cli/commands/invoke.py @@ -7,11 +7,16 @@ import asyncio import json from typing import Optional +from pathlib import Path import typer from rich.console import Console -from mcp_agent.cli.core.utils import load_user_app +from mcp_agent.cli.core.utils import ( + load_user_app, + detect_default_script, + select_servers_from_config, +) from mcp_agent.workflows.factory import create_llm @@ -48,13 +53,13 @@ def invoke( raise typer.Exit(6) async def _run(): - app_obj = load_user_app(Path(script) if script else Path("agent.py")) + script_path = detect_default_script(Path(script) if script else None) + app_obj = load_user_app(script_path) await app_obj.initialize() async with app_obj.run(): if agent: # Run via LLM - server_list = servers.split(",") if servers else [] - server_list = [s.strip() for s in server_list if s.strip()] + server_list = select_servers_from_config(servers, None, None) llm = create_llm( agent_name=agent, server_names=server_list, @@ -105,8 +110,6 @@ async def _run(): val = result console.print(val, end="\n\n\n") - from pathlib import Path - try: asyncio.run(_run()) except KeyboardInterrupt: diff --git a/src/mcp_agent/cli/commands/keys.py b/src/mcp_agent/cli/commands/keys.py index 0acf9bc3b..ea4b0f13f 100644 --- a/src/mcp_agent/cli/commands/keys.py +++ b/src/mcp_agent/cli/commands/keys.py @@ -44,7 +44,7 @@ "claude-3-opus-20240229", "claude-3-haiku-20240307", ], - "test_endpoint": "https://api.anthropic.com/v1/messages", + "test_endpoint": "https://api.anthropic.com/v1/models", "docs": "https://console.anthropic.com/settings/keys", }, "google": { diff --git a/src/mcp_agent/cli/commands/serve.py b/src/mcp_agent/cli/commands/serve.py index fbc2de633..09137eae8 100644 --- a/src/mcp_agent/cli/commands/serve.py +++ b/src/mcp_agent/cli/commands/serve.py @@ -19,7 +19,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn from mcp_agent.server.app_server import create_mcp_server_for_app -from mcp_agent.cli.core.utils import load_user_app +from mcp_agent.cli.core.utils import load_user_app, detect_default_script from mcp_agent.config import get_settings @@ -118,9 +118,9 @@ def serve( Start an MCP server for your app. Examples: - mcp-agent serve --script agent.py - mcp-agent serve --transport http --port 8000 - mcp-agent serve --reload --debug + mcp-agent dev serve --script agent.py + mcp-agent dev serve --transport http --port 8000 + mcp-agent dev serve --reload --debug """ if ctx.invoked_subcommand: @@ -138,12 +138,14 @@ def serve( # Load configuration path is handled after loading app by overriding app settings async def _run(): - # Load the app - script_path = Path(script) if script else Path("agent.py") + # Load the app (auto-detect main.py preferred) + script_path = detect_default_script(Path(script) if script else None) if not script_path.exists(): console.print(f"[red]Script not found: {script_path}[/red]") - console.print("\n[dim]Create an agent.py file or specify --script[/dim]") + console.print( + "\n[dim]Create a main.py (preferred) or agent.py file, or specify --script[/dim]" + ) raise typer.Exit(1) console.print("\n[bold cyan]🚀 MCP-Agent Server[/bold cyan]") @@ -379,10 +381,13 @@ def test( timeout: float = typer.Option(5.0, "--timeout", "-t", help="Test timeout"), ) -> None: """Test if the server can be loaded and initialized.""" - script_path = Path(script) if script else Path("agent.py") + script_path = detect_default_script(Path(script) if script else None) if not script_path.exists(): console.print(f"[red]Script not found: {script_path}[/red]") + console.print( + "\n[dim]Create a main.py (preferred) or agent.py file, or specify --script[/dim]" + ) raise typer.Exit(1) console.print(f"\n[bold]Testing server: {script_path}[/bold]\n") @@ -463,7 +468,7 @@ async def _test(): ) console.print("\n[dim]Server is ready to run with:[/dim]") - console.print(f" [cyan]mcp-agent serve --script {script_path}[/cyan]") + console.print(f" [cyan]mcp-agent dev serve --script {script_path}[/cyan]") except Exception: console.print("\n[red bold]❌ Server test failed[/red bold]") @@ -526,9 +531,11 @@ def generate( console.print("\n[bold]Next steps:[/bold]") console.print(f"1. Edit the server: [cyan]{output}[/cyan]") console.print( - f"2. Test the server: [cyan]mcp-agent serve test --script {output}[/cyan]" + f"2. Test the server: [cyan]mcp-agent dev serve test --script {output}[/cyan]" + ) + console.print( + f"3. Run the server: [cyan]mcp-agent dev serve --script {output}[/cyan]" ) - console.print(f"3. Run the server: [cyan]mcp-agent serve --script {output}[/cyan]") console.print( - f"4. Or serve via HTTP: [cyan]mcp-agent serve --script {output} --transport http --port 8000[/cyan]" + f"4. Or serve via HTTP: [cyan]mcp-agent dev serve --script {output} --transport http --port 8000[/cyan]" ) diff --git a/src/mcp_agent/cli/core/utils.py b/src/mcp_agent/cli/core/utils.py index 3a6b570fe..2743feaa9 100644 --- a/src/mcp_agent/cli/core/utils.py +++ b/src/mcp_agent/cli/core/utils.py @@ -3,10 +3,10 @@ import sys from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, List, Optional from mcp_agent.app import MCPApp -from mcp_agent.config import MCPServerSettings, MCPSettings +from mcp_agent.config import MCPServerSettings, MCPSettings, Settings, get_settings def run_async(coro): @@ -27,13 +27,17 @@ def run_async(coro): raise -def load_user_app(script_path: Path | None) -> MCPApp: +def load_user_app(script_path: Path | None, settings_override: Optional[Settings] = None) -> MCPApp: """Import a user script and return an MCPApp instance. Resolution order within module globals: 1) variable named 'app' that is MCPApp 2) callable 'create_app' or 'get_app' that returns MCPApp 3) first MCPApp instance found in globals + + Args: + script_path: Path to the Python script containing the MCPApp + settings_override: Optional settings to override the app's configuration """ if script_path is None: raise FileNotFoundError("No script specified") @@ -52,6 +56,8 @@ def load_user_app(script_path: Path | None) -> MCPApp: # 1) app variable app_obj = getattr(module, "app", None) if isinstance(app_obj, MCPApp): + if settings_override: + app_obj._config = settings_override return app_obj # 2) factory @@ -60,11 +66,15 @@ def load_user_app(script_path: Path | None) -> MCPApp: if callable(fn): res = fn() if isinstance(res, MCPApp): + if settings_override: + res._config = settings_override return res # 3) scan globals for val in module.__dict__.values(): if isinstance(val, MCPApp): + if settings_override: + val._config = settings_override return val raise RuntimeError( @@ -81,6 +91,57 @@ def ensure_mcp_servers(app: MCPApp) -> None: cfg.mcp.servers = {} +def detect_default_script(explicit: Optional[Path]) -> Path: + """Choose a default script path. + + Preference order: + 1) explicit value if provided + 2) ./main.py + 3) ./agent.py + Returns the first existing file; if none exist, returns the first preference path (main.py). + """ + if explicit: + return explicit + cwd = Path.cwd() + main_candidate = cwd / "main.py" + agent_candidate = cwd / "agent.py" + if main_candidate.exists(): + return main_candidate + if agent_candidate.exists(): + return agent_candidate + # Fall back to main.py (even if missing) so callers can show a helpful message + return main_candidate + + +def select_servers_from_config( + explicit_servers_csv: Optional[str], + url_servers: Optional[Dict[str, Dict[str, Any]]], + stdio_servers: Optional[Dict[str, Dict[str, Any]]], +) -> List[str]: + """Resolve which servers should be active based on inputs and config. + + - If explicit --servers provided, use those + - Else, if dynamic URL/stdio servers provided, use their names + - Else, use all servers from mcp_agent.config.yaml (if present) + """ + if explicit_servers_csv: + items = [s.strip() for s in explicit_servers_csv.split(",") if s.strip()] + return items + + names: List[str] = [] + if url_servers: + names.extend(list(url_servers.keys())) + if stdio_servers: + names.extend(list(stdio_servers.keys())) + if names: + return names + + settings = get_settings() + if settings.mcp and settings.mcp.servers: + return list(settings.mcp.servers.keys()) + return [] + + def attach_url_servers(app: MCPApp, servers: Dict[str, Dict[str, Any]] | None) -> None: """Attach URL-based servers (http/sse/streamable_http) to app config.""" if not servers: diff --git a/src/mcp_agent/cli/main.py b/src/mcp_agent/cli/main.py index 5b6dd359a..599836d0c 100644 --- a/src/mcp_agent/cli/main.py +++ b/src/mcp_agent/cli/main.py @@ -10,6 +10,7 @@ from __future__ import annotations import logging +from pathlib import Path import typer from rich.console import Console @@ -24,7 +25,7 @@ # Local command groups (scaffolded) -from mcp_agent.cli.cloud.commands import deploy_config, login, logout, whoami +from mcp_agent.cli.cloud.commands import deploy_config, login from mcp_agent.cli.commands import ( check as check_cmd, chat as chat_cmd, @@ -65,6 +66,25 @@ cls=HelpfulTyperGroup, ) +# Local development umbrella group +dev_group = typer.Typer( + help="Local development: start app, chat, invoke, serve, servers, build, logs", + no_args_is_help=False, + cls=HelpfulTyperGroup, +) + + +@dev_group.callback(invoke_without_command=True) +def _dev_group_entry( + ctx: typer.Context, + script: Path = typer.Option(None, "--script", help="Entry script"), +): + """If no subcommand is provided, behave like 'dev start'.""" + if ctx.invoked_subcommand: + return + # Delegate to the existing dev implementation + dev_cmd.dev(script=script) + console = Console(stderr=False) err_console = Console(stderr=True) @@ -123,43 +143,47 @@ def main( console.print("Run 'mcp-agent --help' to see all commands.") -# Mount non-cloud command groups +# Mount non-cloud command groups (top-level, curated) app.add_typer(init_cmd.app, name="init", help="Scaffold a new mcp-agent project") app.add_typer(quickstart_cmd.app, name="quickstart", help="Copy curated examples") -app.add_typer(go_cmd.app, name="go", help="Quick interactive agent") -app.add_typer(check_cmd.app, name="check", help="Check configuration and environment") app.add_typer(config_cmd.app, name="config", help="Manage and inspect configuration") -app.add_typer(keys_cmd.app, name="keys", help="Manage provider API keys") -app.add_typer(models_cmd.app, name="models", help="List and manage models") +app.add_typer(doctor_cmd.app, name="doctor", help="Comprehensive diagnostics") -app.add_typer(chat_cmd.app, name="chat", help="Ephemeral REPL for quick iteration") -app.add_typer(dev_cmd.app, name="dev", help="Run app locally with live reload") -app.add_typer( +# Group local dev/runtime commands under `dev` +dev_group.add_typer(dev_cmd.app, name="start", help="Run app locally with live reload") +dev_group.add_typer( + chat_cmd.app, name="chat", help="Ephemeral REPL for quick iteration" +) +dev_group.add_typer( invoke_cmd.app, name="invoke", help="Invoke agent/workflow programmatically" ) -app.add_typer(serve_cmd.app, name="serve", help="Serve app as an MCP server") -app.add_typer(server_cmd.app, name="server", help="Local server helpers") -app.add_typer( +dev_group.add_typer(serve_cmd.app, name="serve", help="Serve app as an MCP server") +dev_group.add_typer(server_cmd.app, name="server", help="Local server helpers") +dev_group.add_typer( build_cmd.app, name="build", help="Preflight and bundle prep for deployment" ) -app.add_typer(logs_cmd.app, name="logs", help="Tail local logs") -app.add_typer(doctor_cmd.app, name="doctor", help="Comprehensive diagnostics") -app.add_typer(configure_cmd.app, name="configure", help="Client integration helpers") +dev_group.add_typer(logs_cmd.app, name="logs", help="Tail local logs") +dev_group.add_typer( + check_cmd.app, name="check", help="Check configuration and environment" +) +dev_group.add_typer(go_cmd.app, name="go", help="Quick interactive agent") +dev_group.add_typer(keys_cmd.app, name="keys", help="Manage provider API keys") +dev_group.add_typer(models_cmd.app, name="models", help="List and manage models") +dev_group.add_typer(configure_cmd.app, name="client", help="Client integration helpers") + +# Mount the dev umbrella group +app.add_typer(dev_group, name="dev", help="Local development and runtime") # Mount cloud commands app.add_typer(cloud_app, name="cloud", help="MCP Agent Cloud commands") -# Register some key cloud commands directly as top-level commands +# Register key cloud commands directly as top-level aliases app.command("deploy", help="Deploy an MCP agent (alias for 'cloud deploy')")( deploy_config ) app.command( "login", help="Authenticate to MCP Agent Cloud API (alias for 'cloud login')" )(login) -app.command( - "whoami", help="Print current identity and org(s) (alias for 'cloud whoami')" -)(whoami) -app.command("logout", help="Clear credentials (alias for 'cloud logout')")(logout) def run() -> None: