diff --git a/src/mcp_agent/cli/commands/__init__.py b/src/mcp_agent/cli/commands/__init__.py index 1c81d0fad..ea81b3d87 100644 --- a/src/mcp_agent/cli/commands/__init__.py +++ b/src/mcp_agent/cli/commands/__init__.py @@ -11,7 +11,6 @@ invoke, serve, init, - quickstart, config, keys, models, @@ -31,7 +30,6 @@ "invoke", "serve", "init", - "quickstart", "config", "keys", "models", diff --git a/src/mcp_agent/cli/commands/init.py b/src/mcp_agent/cli/commands/init.py index 475110ee6..5550e3b67 100644 --- a/src/mcp_agent/cli/commands/init.py +++ b/src/mcp_agent/cli/commands/init.py @@ -1,11 +1,13 @@ """ -Project scaffolding: mcp-agent init (scaffold minimal version). +Project scaffolding: mcp-agent init (scaffold minimal version or copy curated examples). """ from __future__ import annotations +import shutil from pathlib import Path from importlib import resources +from importlib.resources import files as _pkg_files import typer from rich.console import Console @@ -14,6 +16,10 @@ app = typer.Typer(help="Scaffold a new mcp-agent project") console = Console() +err_console = Console(stderr=True) + +# Path to repository examples +EXAMPLE_ROOT = Path(__file__).parents[4] / "examples" def _load_template(template_name: str) -> str: @@ -74,11 +80,70 @@ def _write_readme(dir_path: Path, content: str, force: bool) -> str | None: return None +def _copy_tree(src: Path, dst: Path, force: bool) -> int: + """Copy a directory tree from src to dst. + + Returns 1 on success, 0 on failure. + """ + if not src.exists(): + err_console.print(f"[red]Source not found: {src}[/red]") + return 0 + try: + if dst.exists(): + if force: + shutil.rmtree(dst) + else: + return 0 + shutil.copytree(src, dst) + return 1 + except Exception as e: + err_console.print(f"[red]Error copying tree: {e}[/red]") + return 0 + + +def _copy_pkg_tree(pkg_rel: str, dst: Path, force: bool) -> int: + """Copy packaged examples from mcp_agent.data/examples/ into dst. + + Uses importlib.resources to locate files installed with the package. + Returns 1 on success, 0 on failure. + """ + try: + root = ( + _pkg_files("mcp_agent") + .joinpath("data") + .joinpath("examples") + .joinpath(pkg_rel) + ) + except Exception: + return 0 + if not root.exists(): + return 0 + + # Mirror directory tree + def _copy_any(node, target: Path): + if node.is_dir(): + target.mkdir(parents=True, exist_ok=True) + for child in node.iterdir(): + _copy_any(child, target / child.name) + else: + if target.exists() and not force: + return + with node.open("rb") as rf: + data = rf.read() + target.parent.mkdir(parents=True, exist_ok=True) + with open(target, "wb") as wf: + wf.write(data) + + _copy_any(root, dst) + return 1 + + @app.callback(invoke_without_command=True) def init( ctx: typer.Context, dir: Path = typer.Option(Path("."), "--dir", "-d", help="Target directory"), template: str = typer.Option("basic", "--template", "-t", help="Template to use"), + quickstart: str = typer.Option(None, "--quickstart", help="Quickstart mode: copy example without config files"), force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"), no_gitignore: bool = typer.Option( False, "--no-gitignore", help="Skip creating .gitignore" @@ -87,10 +152,14 @@ def init( False, "--list", "-l", help="List available templates" ), ) -> None: - """Initialize a new MCP-Agent project with configuration and example files.""" + """Initialize a new MCP-Agent project with configuration and example files. + + Use --template for full project initialization with config files. + Use --quickstart for copying examples only.""" # Available templates with descriptions - templates = { + # Organized into scaffolding templates and full example templates + scaffolding_templates = { "basic": "Simple agent with filesystem and fetch capabilities", "server": "MCP server with workflow and parallel agents", "token": "Token counting example with monitoring", @@ -98,23 +167,107 @@ def init( "minimal": "Minimal configuration files only", } + example_templates = { + "workflow": "Workflow examples (from examples/workflows)", + "researcher": "MCP researcher use case (from examples/usecases/mcp_researcher)", + "data-analysis": "Financial data analysis example", + "state-transfer": "Workflow router with state transfer", + "mcp-basic-agent": "Basic MCP agent example", + "token-counter": "Token counting with monitoring", + "agent-factory": "Agent factory pattern", + "basic-agent-server": "Basic agent server (asyncio)", + "reference-agent-server": "Reference agent server implementation", + "elicitation": "Elicitation server example", + "sampling": "Sampling server example", + "notifications": "Notifications server example", + } + + templates = {**scaffolding_templates, **example_templates} + if list_templates: console.print("\n[bold]Available Templates:[/bold]\n") - table = Table(show_header=True, header_style="cyan") - table.add_column("Template", style="green") - table.add_column("Description") - for name, desc in templates.items(): - table.add_row(name, desc) + # Templates table + console.print("[bold cyan]Templates:[/bold cyan]") + console.print("[dim]Creates minimal project structure with config files[/dim]\n") + table1 = Table(show_header=True, header_style="cyan") + table1.add_column("Template", style="green") + table1.add_column("Description") + for name, desc in scaffolding_templates.items(): + table1.add_row(name, desc) + console.print(table1) + + # Quickstart templates table + console.print("\n[bold cyan]Quickstart Templates:[/bold cyan]") + console.print("[dim]Copies complete example projects[/dim]\n") + table2 = Table(show_header=True, header_style="cyan") + table2.add_column("Template", style="green") + table2.add_column("Description") + for name, desc in example_templates.items(): + table2.add_row(name, desc) + console.print(table2) - console.print(table) console.print("\n[dim]Use: mcp-agent init --template [/dim]") return if ctx.invoked_subcommand: return - # Validate template + if quickstart: + if quickstart not in example_templates: + console.print(f"[red]Unknown quickstart example: {quickstart}[/red]") + console.print(f"Available examples: {', '.join(example_templates.keys())}") + console.print("[dim]Use --list to see all available templates[/dim]") + raise typer.Exit(1) + + example_map = { + "workflow": (EXAMPLE_ROOT / "workflows", "workflow"), + "researcher": (EXAMPLE_ROOT / "usecases" / "mcp_researcher", "researcher"), + "data-analysis": (EXAMPLE_ROOT / "usecases" / "mcp_financial_analyzer", "data-analysis"), + "state-transfer": (EXAMPLE_ROOT / "workflows" / "workflow_router", "state-transfer"), + "basic-agent-server": (EXAMPLE_ROOT / "mcp_agent_server" / "asyncio", "basic_agent_server"), + "mcp-basic-agent": (None, "mcp_basic_agent", "basic/mcp_basic_agent"), + "token-counter": (None, "token_counter", "basic/token_counter"), + "agent-factory": (None, "agent_factory", "basic/agent_factory"), + "reference-agent-server": (None, "reference_agent_server", "mcp_agent_server/reference"), + "elicitation": (None, "elicitation", "mcp_agent_server/elicitation"), + "sampling": (None, "sampling", "mcp_agent_server/sampling"), + "notifications": (None, "notifications", "mcp_agent_server/notifications"), + "hello-world": (EXAMPLE_ROOT / "cloud" / "hello_world", "hello_world"), + "mcp": (EXAMPLE_ROOT / "cloud" / "mcp", "mcp"), + "temporal": (EXAMPLE_ROOT / "cloud" / "temporal", "temporal"), + "chatgpt-app": (EXAMPLE_ROOT / "cloud" / "chatgpt_app", "chatgpt_app"), + } + + mapping = example_map.get(quickstart) + if not mapping: + console.print(f"[red]Quickstart example '{quickstart}' not found[/red]") + raise typer.Exit(1) + + base_dir = dir.resolve() + base_dir.mkdir(parents=True, exist_ok=True) + + if len(mapping) == 3: + _, dst_name, pkg_rel = mapping + dst = base_dir / dst_name + copied = _copy_pkg_tree(pkg_rel, dst, force) + if not copied: + src = EXAMPLE_ROOT / pkg_rel.replace("/", "_") + if src.exists(): + copied = _copy_tree(src, dst, force) + else: + src, dst_name = mapping + dst = base_dir / dst_name + copied = _copy_tree(src, dst, force) + + if copied: + console.print(f"Copied {copied} set(s) to {dst}") + else: + console.print(f"[yellow]Could not copy '{quickstart}' - destination may already exist[/yellow]") + console.print("Use --force to overwrite") + + return + if template not in templates: console.print(f"[red]Unknown template: {template}[/red]") console.print(f"Available templates: {', '.join(templates.keys())}") @@ -150,7 +303,61 @@ def init( if gitignore_content and _write(gitignore_path, gitignore_content, force): files_created.append(".gitignore") - # Create template-specific files + # Handle example templates (copy from repository or package) + if template in example_templates: + # Map template names to their source paths + # Format: "name": (repo_path, dest_name) for repo examples + # "name": (None, dest_name, pkg_rel) for packaged examples + example_map = { + "workflow": (EXAMPLE_ROOT / "workflows", "workflow"), + "researcher": (EXAMPLE_ROOT / "usecases" / "mcp_researcher", "researcher"), + "data-analysis": (EXAMPLE_ROOT / "usecases" / "mcp_financial_analyzer", "data-analysis"), + "state-transfer": (EXAMPLE_ROOT / "workflows" / "workflow_router", "state-transfer"), + "basic-agent-server": (EXAMPLE_ROOT / "mcp_agent_server" / "asyncio", "basic_agent_server"), + "mcp-basic-agent": (None, "mcp_basic_agent", "basic/mcp_basic_agent"), + "token-counter": (None, "token_counter", "basic/token_counter"), + "agent-factory": (None, "agent_factory", "basic/agent_factory"), + "reference-agent-server": (None, "reference_agent_server", "mcp_agent_server/reference"), + "elicitation": (None, "elicitation", "mcp_agent_server/elicitation"), + "sampling": (None, "sampling", "mcp_agent_server/sampling"), + "notifications": (None, "notifications", "mcp_agent_server/notifications"), + "hello-world": (EXAMPLE_ROOT / "cloud" / "hello_world", "hello_world"), + "mcp": (EXAMPLE_ROOT / "cloud" / "mcp", "mcp"), + "temporal": (EXAMPLE_ROOT / "cloud" / "temporal", "temporal"), + "chatgpt-app": (EXAMPLE_ROOT / "cloud" / "chatgpt_app", "chatgpt_app"), + } + + mapping = example_map.get(template) + if not mapping: + console.print(f"[red]Example template '{template}' not found[/red]") + raise typer.Exit(1) + + if len(mapping) == 3: + _, dst_name, pkg_rel = mapping + dst = dir / dst_name + copied = _copy_pkg_tree(pkg_rel, dst, force) + if not copied: + src = EXAMPLE_ROOT / pkg_rel.replace("/", "_") + if src.exists(): + copied = _copy_tree(src, dst, force) + else: + src, dst_name = mapping + dst = dir / dst_name + copied = _copy_tree(src, dst, force) + + if copied: + console.print(f"\n[green]✅ Successfully copied example '{template}'![/green]") + console.print(f"Created: [cyan]{dst}[/cyan]\n") + console.print("[bold]Next steps:[/bold]") + console.print(f"1. cd [cyan]{dst}[/cyan]") + console.print("2. Review the README for instructions") + console.print("3. Add your API keys to config/secrets files if needed") + else: + console.print(f"[yellow]Example '{template}' could not be copied[/yellow]") + console.print("The destination may already exist. Use --force to overwrite.") + + return + if template == "basic": # Determine entry script name and handle existing files script_name = "main.py" @@ -278,7 +485,7 @@ def init( 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]") + console.print(" See examples: [cyan]mcp-agent init --list[/cyan]") console.print( "\n[dim]Run [cyan]mcp-agent doctor[/cyan] to check your configuration[/dim]" @@ -346,10 +553,12 @@ def interactive( console.print(f"\n[bold]Creating project '{project_name}'...[/bold]") # Use the main init function with selected options + ctx = typer.Context(init) init( - ctx=typer.Context(), + ctx=ctx, dir=dir, template=template_name, + quickstart=None, force=False, no_gitignore=False, list_templates=False, diff --git a/src/mcp_agent/cli/commands/quickstart.py b/src/mcp_agent/cli/commands/quickstart.py deleted file mode 100644 index 854f68c0a..000000000 --- a/src/mcp_agent/cli/commands/quickstart.py +++ /dev/null @@ -1,248 +0,0 @@ -""" -Quickstart examples: scaffolded adapters over repository examples. -""" - -from __future__ import annotations - -import shutil -from pathlib import Path -from importlib.resources import files as _pkg_files - -import typer -from rich.console import Console -from rich.table import Table - - -app = typer.Typer(help="Copy curated examples") -console = Console() - - -EXAMPLE_ROOT = Path(__file__).parents[4] / "examples" - - -def _copy_tree(src: Path, dst: Path, force: bool) -> int: - if not src.exists(): - typer.echo(f"Source not found: {src}", err=True) - return 0 - try: - if dst.exists(): - if force: - shutil.rmtree(dst) - else: - return 0 - shutil.copytree(src, dst) - return 1 - except Exception: - return 0 - - -def _copy_pkg_tree(pkg_rel: str, dst: Path, force: bool) -> int: - """Copy packaged examples from mcp_agent.data/examples/ into dst. - - Uses importlib.resources to locate files installed with the package. - """ - try: - root = ( - _pkg_files("mcp_agent") - .joinpath("data") - .joinpath("examples") - .joinpath(pkg_rel) - ) - except Exception: - return 0 - if not root.exists(): - return 0 - - # Mirror directory tree - def _copy_any(node, target: Path): - if node.is_dir(): - target.mkdir(parents=True, exist_ok=True) - for child in node.iterdir(): - _copy_any(child, target / child.name) - else: - if target.exists() and not force: - return - with node.open("rb") as rf: - data = rf.read() - target.parent.mkdir(parents=True, exist_ok=True) - with open(target, "wb") as wf: - wf.write(data) - - _copy_any(root, dst) - return 1 - - -@app.callback(invoke_without_command=True) -def overview() -> None: - table = Table(title="Quickstarts") - table.add_column("Name") - table.add_column("Path") - rows = [ - ("workflow", "examples/workflows"), - ("researcher", "examples/usecases/mcp_researcher"), - ("data-analysis", "examples/usecases/mcp_financial_analyzer"), - ("state-transfer", "examples/workflows/workflow_router"), - ("mcp-basic-agent", "data/examples/basic/mcp_basic_agent"), - ("token-counter", "data/examples/basic/token_counter"), - ("agent-factory", "data/examples/basic/agent_factory"), - ("basic-agent-server", "data/examples/mcp_agent_server/asyncio"), - ("reference-agent-server", "data/examples/mcp_agent_server/reference"), - ("elicitation", "data/examples/mcp_agent_server/elicitation"), - ("sampling", "data/examples/mcp_agent_server/sampling"), - ("notifications", "data/examples/mcp_agent_server/notifications"), - ] - for n, p in rows: - table.add_row(n, p) - console.print(table) - - -@app.command() -def workflow( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - src = EXAMPLE_ROOT / "workflows" - dst = dir.resolve() / "workflow" - copied = _copy_tree(src, dst, force) - console.print(f"Copied {copied} set(s) to {dst}") - - -@app.command() -def researcher( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - src = EXAMPLE_ROOT / "usecases" / "mcp_researcher" - dst = dir.resolve() / "researcher" - copied = _copy_tree(src, dst, force) - console.print(f"Copied {copied} set(s) to {dst}") - - -@app.command("elicitations") -def elicitations_qs( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - src = EXAMPLE_ROOT.parent / "mcp" / "elicitations" - dst = dir.resolve() / "elicitations" - copied = _copy_tree(src, dst, force) - console.print(f"Copied {copied} set(s) to {dst}") - - -@app.command("state-transfer") -def state_transfer( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - src = EXAMPLE_ROOT / "workflows" / "workflow_router" - dst = dir.resolve() / "state-transfer" - copied = _copy_tree(src, dst, force) - console.print(f"Copied {copied} set(s) to {dst}") - - -@app.command("data-analysis") -def data_analysis( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - # Map to financial analyzer example as the closest match - src = EXAMPLE_ROOT / "usecases" / "mcp_financial_analyzer" - dst = dir.resolve() / "data-analysis" - copied = _copy_tree(src, dst, force) - console.print(f"Copied {copied} set(s) to {dst}") - - -@app.command("mcp-basic-agent") -def mcp_basic_agent( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - dst = dir.resolve() / "mcp_basic_agent" - copied = _copy_pkg_tree("basic/mcp_basic_agent", dst, force) - if not copied: - # fallback to repo examples - src = EXAMPLE_ROOT / "basic" / "mcp_basic_agent" - copied = _copy_tree(src, dst, force) - console.print(f"Copied {copied} set(s) to {dst}") - - -@app.command("token-counter") -def token_counter( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - dst = dir.resolve() / "token_counter" - copied = _copy_pkg_tree("basic/token_counter", dst, force) - if not copied: - src = EXAMPLE_ROOT / "basic" / "token_counter" - copied = _copy_tree(src, dst, force) - console.print(f"Copied {copied} set(s) to {dst}") - - -@app.command("agent-factory") -def agent_factory( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - dst = dir.resolve() / "agent_factory" - copied = _copy_pkg_tree("basic/agent_factory", dst, force) - if not copied: - src = EXAMPLE_ROOT / "basic" / "agent_factory" - copied = _copy_tree(src, dst, force) - console.print(f"Copied {copied} set(s) to {dst}") - - -@app.command("basic-agent-server") -def basic_agent_server( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - dst = dir.resolve() / "basic_agent_server" - copied = _copy_pkg_tree("mcp_agent_server/asyncio", dst, force) - if not copied: - src = EXAMPLE_ROOT / "mcp_agent_server" / "asyncio" - copied = _copy_tree(src, dst, force) - console.print(f"Copied {copied} set(s) to {dst}") - - -@app.command("reference-agent-server") -def reference_agent_server( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - dst = dir.resolve() / "reference_agent_server" - copied = _copy_pkg_tree("mcp_agent_server/reference", dst, force) - if not copied: - src = EXAMPLE_ROOT / "mcp_agent_server" / "reference" - copied = _copy_tree(src, dst, force) - console.print(f"Copied {copied} set(s) to {dst}") - - -@app.command("elicitation") -def elicitation( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - dst = dir.resolve() / "elicitation" - copied = _copy_pkg_tree("mcp_agent_server/elicitation", dst, force) - console.print(f"Copied {copied} set(s) to {dst}") - - -@app.command("sampling") -def sampling( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - dst = dir.resolve() / "sampling" - copied = _copy_pkg_tree("mcp_agent_server/sampling", dst, force) - console.print(f"Copied {copied} set(s) to {dst}") - - -@app.command("notifications") -def notifications( - dir: Path = typer.Argument(Path(".")), - force: bool = typer.Option(False, "--force", "-f"), -) -> None: - dst = dir.resolve() / "notifications" - copied = _copy_pkg_tree("mcp_agent_server/notifications", dst, force) - console.print(f"Copied {copied} set(s) to {dst}") diff --git a/src/mcp_agent/cli/main.py b/src/mcp_agent/cli/main.py index 664870eeb..430f0baad 100644 --- a/src/mcp_agent/cli/main.py +++ b/src/mcp_agent/cli/main.py @@ -55,9 +55,6 @@ from mcp_agent.cli.commands import ( models as models_cmd, ) -from mcp_agent.cli.commands import ( - quickstart as quickstart_cmd, -) from mcp_agent.cli.utils.typer_utils import HelpfulTyperGroup app = typer.Typer( @@ -145,8 +142,7 @@ def main( # 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(init_cmd.app, name="init", help="Scaffold a new mcp-agent project or copy curated examples") app.add_typer(config_cmd.app, name="config", help="Manage and inspect configuration") app.add_typer(doctor_cmd.app, name="doctor", help="Comprehensive diagnostics")