Skip to content

Commit ad2b293

Browse files
jtcorbettandrew-lastmile
authored andcommitted
merge init and quickstart (lastmile-ai#569)
### TL;DR Unified project initialization by merging `init` and `quickstart` commands into a single enhanced `init` command with improved template management. ### What changed? - Consolidated `mcp-agent quickstart` functionality into `mcp-agent init` with a new `--quickstart` flag for backward compatibility - Organized templates into two categories: scaffolding templates and example templates - Added `--list` option to display all available templates with descriptions - Enhanced CLI options with short forms (`-d`, `-t`, `-f`, `-l`) - Added new option `--no-gitignore` to skip creating .gitignore files - Improved documentation with clearer examples and template descriptions - Removed the standalone `quickstart.py` module as its functionality is now in `init.py` - Updated CLI reference documentation to reflect these changes ### How to test? ```bash # List all available templates mcp-agent init --list # Create scaffolding project with config files mcp-agent init --template basic # Copy complete example with config files mcp-agent init --template workflow --dir ./my-project # Use quickstart mode (backward compatible with old command) mcp-agent init --quickstart workflow ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added `--quickstart` option to the init command for copying curated example templates alongside project scaffolding. - Example templates now available during project initialization. - **Chores** - Consolidated quickstart functionality into the init command; separate quickstart command group removed. - Updated init help text to reflect dual-mode scaffolding capabilities. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent ba19f83 commit ad2b293

File tree

4 files changed

+223
-268
lines changed

4 files changed

+223
-268
lines changed

src/mcp_agent/cli/commands/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
invoke,
1212
serve,
1313
init,
14-
quickstart,
1514
config,
1615
keys,
1716
models,
@@ -31,7 +30,6 @@
3130
"invoke",
3231
"serve",
3332
"init",
34-
"quickstart",
3533
"config",
3634
"keys",
3735
"models",

src/mcp_agent/cli/commands/init.py

Lines changed: 222 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""
2-
Project scaffolding: mcp-agent init (scaffold minimal version).
2+
Project scaffolding: mcp-agent init (scaffold minimal version or copy curated examples).
33
"""
44

55
from __future__ import annotations
66

7+
import shutil
78
from pathlib import Path
89
from importlib import resources
10+
from importlib.resources import files as _pkg_files
911

1012
import typer
1113
from rich.console import Console
@@ -14,6 +16,10 @@
1416

1517
app = typer.Typer(help="Scaffold a new mcp-agent project")
1618
console = Console()
19+
err_console = Console(stderr=True)
20+
21+
# Path to repository examples
22+
EXAMPLE_ROOT = Path(__file__).parents[4] / "examples"
1723

1824

1925
def _load_template(template_name: str) -> str:
@@ -74,11 +80,70 @@ def _write_readme(dir_path: Path, content: str, force: bool) -> str | None:
7480
return None
7581

7682

83+
def _copy_tree(src: Path, dst: Path, force: bool) -> int:
84+
"""Copy a directory tree from src to dst.
85+
86+
Returns 1 on success, 0 on failure.
87+
"""
88+
if not src.exists():
89+
err_console.print(f"[red]Source not found: {src}[/red]")
90+
return 0
91+
try:
92+
if dst.exists():
93+
if force:
94+
shutil.rmtree(dst)
95+
else:
96+
return 0
97+
shutil.copytree(src, dst)
98+
return 1
99+
except Exception as e:
100+
err_console.print(f"[red]Error copying tree: {e}[/red]")
101+
return 0
102+
103+
104+
def _copy_pkg_tree(pkg_rel: str, dst: Path, force: bool) -> int:
105+
"""Copy packaged examples from mcp_agent.data/examples/<pkg_rel> into dst.
106+
107+
Uses importlib.resources to locate files installed with the package.
108+
Returns 1 on success, 0 on failure.
109+
"""
110+
try:
111+
root = (
112+
_pkg_files("mcp_agent")
113+
.joinpath("data")
114+
.joinpath("examples")
115+
.joinpath(pkg_rel)
116+
)
117+
except Exception:
118+
return 0
119+
if not root.exists():
120+
return 0
121+
122+
# Mirror directory tree
123+
def _copy_any(node, target: Path):
124+
if node.is_dir():
125+
target.mkdir(parents=True, exist_ok=True)
126+
for child in node.iterdir():
127+
_copy_any(child, target / child.name)
128+
else:
129+
if target.exists() and not force:
130+
return
131+
with node.open("rb") as rf:
132+
data = rf.read()
133+
target.parent.mkdir(parents=True, exist_ok=True)
134+
with open(target, "wb") as wf:
135+
wf.write(data)
136+
137+
_copy_any(root, dst)
138+
return 1
139+
140+
77141
@app.callback(invoke_without_command=True)
78142
def init(
79143
ctx: typer.Context,
80144
dir: Path = typer.Option(Path("."), "--dir", "-d", help="Target directory"),
81145
template: str = typer.Option("basic", "--template", "-t", help="Template to use"),
146+
quickstart: str = typer.Option(None, "--quickstart", help="Quickstart mode: copy example without config files"),
82147
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
83148
no_gitignore: bool = typer.Option(
84149
False, "--no-gitignore", help="Skip creating .gitignore"
@@ -87,34 +152,122 @@ def init(
87152
False, "--list", "-l", help="List available templates"
88153
),
89154
) -> None:
90-
"""Initialize a new MCP-Agent project with configuration and example files."""
155+
"""Initialize a new MCP-Agent project with configuration and example files.
156+
157+
Use --template for full project initialization with config files.
158+
Use --quickstart for copying examples only."""
91159

92160
# Available templates with descriptions
93-
templates = {
161+
# Organized into scaffolding templates and full example templates
162+
scaffolding_templates = {
94163
"basic": "Simple agent with filesystem and fetch capabilities",
95164
"server": "MCP server with workflow and parallel agents",
96165
"token": "Token counting example with monitoring",
97166
"factory": "Agent factory with router-based selection",
98167
"minimal": "Minimal configuration files only",
99168
}
100169

170+
example_templates = {
171+
"workflow": "Workflow examples (from examples/workflows)",
172+
"researcher": "MCP researcher use case (from examples/usecases/mcp_researcher)",
173+
"data-analysis": "Financial data analysis example",
174+
"state-transfer": "Workflow router with state transfer",
175+
"mcp-basic-agent": "Basic MCP agent example",
176+
"token-counter": "Token counting with monitoring",
177+
"agent-factory": "Agent factory pattern",
178+
"basic-agent-server": "Basic agent server (asyncio)",
179+
"reference-agent-server": "Reference agent server implementation",
180+
"elicitation": "Elicitation server example",
181+
"sampling": "Sampling server example",
182+
"notifications": "Notifications server example",
183+
}
184+
185+
templates = {**scaffolding_templates, **example_templates}
186+
101187
if list_templates:
102188
console.print("\n[bold]Available Templates:[/bold]\n")
103-
table = Table(show_header=True, header_style="cyan")
104-
table.add_column("Template", style="green")
105-
table.add_column("Description")
106189

107-
for name, desc in templates.items():
108-
table.add_row(name, desc)
190+
# Templates table
191+
console.print("[bold cyan]Templates:[/bold cyan]")
192+
console.print("[dim]Creates minimal project structure with config files[/dim]\n")
193+
table1 = Table(show_header=True, header_style="cyan")
194+
table1.add_column("Template", style="green")
195+
table1.add_column("Description")
196+
for name, desc in scaffolding_templates.items():
197+
table1.add_row(name, desc)
198+
console.print(table1)
199+
200+
# Quickstart templates table
201+
console.print("\n[bold cyan]Quickstart Templates:[/bold cyan]")
202+
console.print("[dim]Copies complete example projects[/dim]\n")
203+
table2 = Table(show_header=True, header_style="cyan")
204+
table2.add_column("Template", style="green")
205+
table2.add_column("Description")
206+
for name, desc in example_templates.items():
207+
table2.add_row(name, desc)
208+
console.print(table2)
109209

110-
console.print(table)
111210
console.print("\n[dim]Use: mcp-agent init --template <name>[/dim]")
112211
return
113212

114213
if ctx.invoked_subcommand:
115214
return
116215

117-
# Validate template
216+
if quickstart:
217+
if quickstart not in example_templates:
218+
console.print(f"[red]Unknown quickstart example: {quickstart}[/red]")
219+
console.print(f"Available examples: {', '.join(example_templates.keys())}")
220+
console.print("[dim]Use --list to see all available templates[/dim]")
221+
raise typer.Exit(1)
222+
223+
example_map = {
224+
"workflow": (EXAMPLE_ROOT / "workflows", "workflow"),
225+
"researcher": (EXAMPLE_ROOT / "usecases" / "mcp_researcher", "researcher"),
226+
"data-analysis": (EXAMPLE_ROOT / "usecases" / "mcp_financial_analyzer", "data-analysis"),
227+
"state-transfer": (EXAMPLE_ROOT / "workflows" / "workflow_router", "state-transfer"),
228+
"basic-agent-server": (EXAMPLE_ROOT / "mcp_agent_server" / "asyncio", "basic_agent_server"),
229+
"mcp-basic-agent": (None, "mcp_basic_agent", "basic/mcp_basic_agent"),
230+
"token-counter": (None, "token_counter", "basic/token_counter"),
231+
"agent-factory": (None, "agent_factory", "basic/agent_factory"),
232+
"reference-agent-server": (None, "reference_agent_server", "mcp_agent_server/reference"),
233+
"elicitation": (None, "elicitation", "mcp_agent_server/elicitation"),
234+
"sampling": (None, "sampling", "mcp_agent_server/sampling"),
235+
"notifications": (None, "notifications", "mcp_agent_server/notifications"),
236+
"hello-world": (EXAMPLE_ROOT / "cloud" / "hello_world", "hello_world"),
237+
"mcp": (EXAMPLE_ROOT / "cloud" / "mcp", "mcp"),
238+
"temporal": (EXAMPLE_ROOT / "cloud" / "temporal", "temporal"),
239+
"chatgpt-app": (EXAMPLE_ROOT / "cloud" / "chatgpt_app", "chatgpt_app"),
240+
}
241+
242+
mapping = example_map.get(quickstart)
243+
if not mapping:
244+
console.print(f"[red]Quickstart example '{quickstart}' not found[/red]")
245+
raise typer.Exit(1)
246+
247+
base_dir = dir.resolve()
248+
base_dir.mkdir(parents=True, exist_ok=True)
249+
250+
if len(mapping) == 3:
251+
_, dst_name, pkg_rel = mapping
252+
dst = base_dir / dst_name
253+
copied = _copy_pkg_tree(pkg_rel, dst, force)
254+
if not copied:
255+
src = EXAMPLE_ROOT / pkg_rel.replace("/", "_")
256+
if src.exists():
257+
copied = _copy_tree(src, dst, force)
258+
else:
259+
src, dst_name = mapping
260+
dst = base_dir / dst_name
261+
copied = _copy_tree(src, dst, force)
262+
263+
if copied:
264+
console.print(f"Copied {copied} set(s) to {dst}")
265+
else:
266+
console.print(f"[yellow]Could not copy '{quickstart}' - destination may already exist[/yellow]")
267+
console.print("Use --force to overwrite")
268+
269+
return
270+
118271
if template not in templates:
119272
console.print(f"[red]Unknown template: {template}[/red]")
120273
console.print(f"Available templates: {', '.join(templates.keys())}")
@@ -150,7 +303,61 @@ def init(
150303
if gitignore_content and _write(gitignore_path, gitignore_content, force):
151304
files_created.append(".gitignore")
152305

153-
# Create template-specific files
306+
# Handle example templates (copy from repository or package)
307+
if template in example_templates:
308+
# Map template names to their source paths
309+
# Format: "name": (repo_path, dest_name) for repo examples
310+
# "name": (None, dest_name, pkg_rel) for packaged examples
311+
example_map = {
312+
"workflow": (EXAMPLE_ROOT / "workflows", "workflow"),
313+
"researcher": (EXAMPLE_ROOT / "usecases" / "mcp_researcher", "researcher"),
314+
"data-analysis": (EXAMPLE_ROOT / "usecases" / "mcp_financial_analyzer", "data-analysis"),
315+
"state-transfer": (EXAMPLE_ROOT / "workflows" / "workflow_router", "state-transfer"),
316+
"basic-agent-server": (EXAMPLE_ROOT / "mcp_agent_server" / "asyncio", "basic_agent_server"),
317+
"mcp-basic-agent": (None, "mcp_basic_agent", "basic/mcp_basic_agent"),
318+
"token-counter": (None, "token_counter", "basic/token_counter"),
319+
"agent-factory": (None, "agent_factory", "basic/agent_factory"),
320+
"reference-agent-server": (None, "reference_agent_server", "mcp_agent_server/reference"),
321+
"elicitation": (None, "elicitation", "mcp_agent_server/elicitation"),
322+
"sampling": (None, "sampling", "mcp_agent_server/sampling"),
323+
"notifications": (None, "notifications", "mcp_agent_server/notifications"),
324+
"hello-world": (EXAMPLE_ROOT / "cloud" / "hello_world", "hello_world"),
325+
"mcp": (EXAMPLE_ROOT / "cloud" / "mcp", "mcp"),
326+
"temporal": (EXAMPLE_ROOT / "cloud" / "temporal", "temporal"),
327+
"chatgpt-app": (EXAMPLE_ROOT / "cloud" / "chatgpt_app", "chatgpt_app"),
328+
}
329+
330+
mapping = example_map.get(template)
331+
if not mapping:
332+
console.print(f"[red]Example template '{template}' not found[/red]")
333+
raise typer.Exit(1)
334+
335+
if len(mapping) == 3:
336+
_, dst_name, pkg_rel = mapping
337+
dst = dir / dst_name
338+
copied = _copy_pkg_tree(pkg_rel, dst, force)
339+
if not copied:
340+
src = EXAMPLE_ROOT / pkg_rel.replace("/", "_")
341+
if src.exists():
342+
copied = _copy_tree(src, dst, force)
343+
else:
344+
src, dst_name = mapping
345+
dst = dir / dst_name
346+
copied = _copy_tree(src, dst, force)
347+
348+
if copied:
349+
console.print(f"\n[green]✅ Successfully copied example '{template}'![/green]")
350+
console.print(f"Created: [cyan]{dst}[/cyan]\n")
351+
console.print("[bold]Next steps:[/bold]")
352+
console.print(f"1. cd [cyan]{dst}[/cyan]")
353+
console.print("2. Review the README for instructions")
354+
console.print("3. Add your API keys to config/secrets files if needed")
355+
else:
356+
console.print(f"[yellow]Example '{template}' could not be copied[/yellow]")
357+
console.print("The destination may already exist. Use --force to overwrite.")
358+
359+
return
360+
154361
if template == "basic":
155362
# Determine entry script name and handle existing files
156363
script_name = "main.py"
@@ -278,7 +485,7 @@ def init(
278485
console.print("4. Run the factory: [cyan]uv run factory.py[/cyan]")
279486
elif template == "minimal":
280487
console.print("3. Create your agent script")
281-
console.print(" See examples: [cyan]mcp-agent quickstart[/cyan]")
488+
console.print(" See examples: [cyan]mcp-agent init --list[/cyan]")
282489

283490
console.print(
284491
"\n[dim]Run [cyan]mcp-agent doctor[/cyan] to check your configuration[/dim]"
@@ -346,10 +553,12 @@ def interactive(
346553
console.print(f"\n[bold]Creating project '{project_name}'...[/bold]")
347554

348555
# Use the main init function with selected options
556+
ctx = typer.Context(init)
349557
init(
350-
ctx=typer.Context(),
558+
ctx=ctx,
351559
dir=dir,
352560
template=template_name,
561+
quickstart=None,
353562
force=False,
354563
no_gitignore=False,
355564
list_templates=False,

0 commit comments

Comments
 (0)