fast-agent-mcp 0.2.40__py3-none-any.whl → 0.2.42__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

Files changed (45) hide show
  1. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.42.dist-info}/METADATA +2 -1
  2. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.42.dist-info}/RECORD +45 -40
  3. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.42.dist-info}/entry_points.txt +2 -2
  4. mcp_agent/agents/base_agent.py +111 -1
  5. mcp_agent/cli/__main__.py +29 -3
  6. mcp_agent/cli/commands/check_config.py +140 -81
  7. mcp_agent/cli/commands/go.py +151 -38
  8. mcp_agent/cli/commands/quickstart.py +6 -2
  9. mcp_agent/cli/commands/server_helpers.py +106 -0
  10. mcp_agent/cli/constants.py +25 -0
  11. mcp_agent/cli/main.py +1 -1
  12. mcp_agent/config.py +111 -44
  13. mcp_agent/core/agent_app.py +104 -15
  14. mcp_agent/core/agent_types.py +5 -1
  15. mcp_agent/core/direct_decorators.py +38 -0
  16. mcp_agent/core/direct_factory.py +18 -4
  17. mcp_agent/core/enhanced_prompt.py +173 -13
  18. mcp_agent/core/fastagent.py +4 -0
  19. mcp_agent/core/interactive_prompt.py +37 -37
  20. mcp_agent/core/usage_display.py +11 -1
  21. mcp_agent/core/validation.py +21 -2
  22. mcp_agent/human_input/elicitation_form.py +53 -21
  23. mcp_agent/llm/augmented_llm.py +28 -9
  24. mcp_agent/llm/augmented_llm_silent.py +48 -0
  25. mcp_agent/llm/model_database.py +20 -0
  26. mcp_agent/llm/model_factory.py +21 -0
  27. mcp_agent/llm/provider_key_manager.py +22 -8
  28. mcp_agent/llm/provider_types.py +20 -12
  29. mcp_agent/llm/providers/augmented_llm_anthropic.py +7 -2
  30. mcp_agent/llm/providers/augmented_llm_azure.py +7 -1
  31. mcp_agent/llm/providers/augmented_llm_bedrock.py +1787 -0
  32. mcp_agent/llm/providers/augmented_llm_google_native.py +4 -1
  33. mcp_agent/llm/providers/augmented_llm_openai.py +12 -3
  34. mcp_agent/llm/providers/augmented_llm_xai.py +38 -0
  35. mcp_agent/llm/usage_tracking.py +28 -3
  36. mcp_agent/logging/logger.py +7 -0
  37. mcp_agent/mcp/hf_auth.py +32 -4
  38. mcp_agent/mcp/mcp_agent_client_session.py +2 -0
  39. mcp_agent/mcp/mcp_aggregator.py +38 -44
  40. mcp_agent/mcp/sampling.py +15 -11
  41. mcp_agent/resources/examples/mcp/elicitations/forms_demo.py +0 -6
  42. mcp_agent/resources/examples/workflows/router.py +9 -0
  43. mcp_agent/ui/console_display.py +125 -13
  44. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.42.dist-info}/WHEEL +0 -0
  45. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.42.dist-info}/licenses/LICENSE +0 -0
@@ -23,30 +23,14 @@ console = Console()
23
23
 
24
24
 
25
25
  def find_config_files(start_path: Path) -> dict[str, Optional[Path]]:
26
- """Find FastAgent configuration files in current directory or parents."""
27
- results = {
28
- "config": None,
29
- "secrets": None,
30
- }
31
-
32
- current = start_path
33
- while current != current.parent: # Stop at root directory
34
- config_path = current / "fastagent.config.yaml"
35
- secrets_path = current / "fastagent.secrets.yaml"
36
-
37
- if config_path.exists() and results["config"] is None:
38
- results["config"] = config_path
39
-
40
- if secrets_path.exists() and results["secrets"] is None:
41
- results["secrets"] = secrets_path
26
+ """Find FastAgent configuration files, preferring secrets file next to config file."""
27
+ from mcp_agent.config import find_fastagent_config_files
42
28
 
43
- # Stop searching if we found both files
44
- if results["config"] is not None and results["secrets"] is not None:
45
- break
46
-
47
- current = current.parent
48
-
49
- return results
29
+ config_path, secrets_path = find_fastagent_config_files(start_path)
30
+ return {
31
+ "config": config_path,
32
+ "secrets": secrets_path,
33
+ }
50
34
 
51
35
 
52
36
  def get_system_info() -> dict:
@@ -109,13 +93,23 @@ def check_api_keys(secrets_summary: dict, config_summary: dict) -> dict:
109
93
  secrets_status = secrets_summary.get("status", "not_found")
110
94
  # Get config if available
111
95
  config = config_summary if config_summary.get("status") == "parsed" else {}
96
+
112
97
  config_azure = {}
113
98
  if config and "azure" in config.get("config", {}):
114
99
  config_azure = config["config"]["azure"]
115
100
 
116
- for provider_value in results:
101
+ for provider in results:
102
+ # Always check environment variables first
103
+ env_key_name = ProviderKeyManager.get_env_key_name(provider)
104
+ env_key_value = os.environ.get(env_key_name)
105
+ if env_key_value:
106
+ if len(env_key_value) > 5:
107
+ results[provider]["env"] = f"...{env_key_value[-5:]}"
108
+ else:
109
+ results[provider]["env"] = "...***"
110
+
117
111
  # Special handling for Azure: support api_key and DefaultAzureCredential
118
- if provider_value == "azure":
112
+ if provider == "azure":
119
113
  # Prefer secrets if present, else fallback to config
120
114
  azure_cfg = {}
121
115
  if secrets_status == "parsed" and "azure" in secrets:
@@ -125,40 +119,18 @@ def check_api_keys(secrets_summary: dict, config_summary: dict) -> dict:
125
119
 
126
120
  use_default_cred = azure_cfg.get("use_default_azure_credential", False)
127
121
  base_url = azure_cfg.get("base_url")
128
- api_key = azure_cfg.get("api_key")
129
- # DefaultAzureCredential mode
130
122
  if use_default_cred and base_url:
131
- results[provider_value]["config"] = "DefaultAzureCredential"
132
- # API key mode (retrocompatible)
133
- if api_key and api_key != API_KEY_HINT_TEXT:
134
- if len(api_key) > 5:
135
- if results[provider_value]["config"]:
136
- results[provider_value]["config"] += " + api_key"
137
- else:
138
- results[provider_value]["config"] = f"...{api_key[-5:]}"
123
+ results[provider]["config"] = "DefaultAzureCredential"
124
+ continue
125
+
126
+ # Check secrets file if it was parsed successfully
127
+ if secrets_status == "parsed":
128
+ config_key = ProviderKeyManager.get_config_file_key(provider, secrets)
129
+ if config_key and config_key != API_KEY_HINT_TEXT:
130
+ if len(config_key) > 5:
131
+ results[provider]["config"] = f"...{config_key[-5:]}"
139
132
  else:
140
- if results[provider_value]["config"]:
141
- results[provider_value]["config"] += " + api_key"
142
- else:
143
- results[provider_value]["config"] = "...***"
144
- else:
145
- # Check environment variables using ProviderKeyManager
146
- env_key_name = ProviderKeyManager.get_env_key_name(provider_value)
147
- env_key_value = os.environ.get(env_key_name)
148
- if env_key_value:
149
- if len(env_key_value) > 5:
150
- results[provider_value]["env"] = f"...{env_key_value[-5:]}"
151
- else:
152
- results[provider_value]["env"] = "...***"
153
-
154
- # Check secrets file if it was parsed successfully
155
- if secrets_status == "parsed":
156
- config_key = ProviderKeyManager.get_config_file_key(provider_value, secrets)
157
- if config_key and config_key != API_KEY_HINT_TEXT:
158
- if len(config_key) > 5:
159
- results[provider_value]["config"] = f"...{config_key[-5:]}"
160
- else:
161
- results[provider_value]["config"] = "...***"
133
+ results[provider]["config"] = "...***"
162
134
 
163
135
  return results
164
136
 
@@ -173,11 +145,24 @@ def get_fastagent_version() -> str:
173
145
 
174
146
  def get_config_summary(config_path: Optional[Path]) -> dict:
175
147
  """Extract key information from the configuration file."""
148
+ from mcp_agent.config import Settings
149
+
150
+ # Get actual defaults from Settings class
151
+ default_settings = Settings()
152
+
176
153
  result = {
177
154
  "status": "not_found", # Default status: not found
178
155
  "error": None,
179
- "default_model": "haiku (system default)",
180
- "logger": {},
156
+ "default_model": default_settings.default_model,
157
+ "logger": {
158
+ "level": default_settings.logger.level,
159
+ "type": default_settings.logger.type,
160
+ "progress_display": default_settings.logger.progress_display,
161
+ "show_chat": default_settings.logger.show_chat,
162
+ "show_tools": default_settings.logger.show_tools,
163
+ "truncate_tools": default_settings.logger.truncate_tools,
164
+ "enable_markup": default_settings.logger.enable_markup,
165
+ },
181
166
  "mcp_servers": [],
182
167
  }
183
168
 
@@ -207,11 +192,19 @@ def get_config_summary(config_path: Optional[Path]) -> dict:
207
192
  if "logger" in config:
208
193
  logger_config = config["logger"]
209
194
  result["logger"] = {
210
- "level": logger_config.get("level", "info (default)"),
211
- "type": logger_config.get("type", "console (default)"),
212
- "progress_display": str(logger_config.get("progress_display", True)),
213
- "show_chat": str(logger_config.get("show_chat", True)),
214
- "show_tools": str(logger_config.get("show_tools", True)),
195
+ "level": logger_config.get("level", default_settings.logger.level),
196
+ "type": logger_config.get("type", default_settings.logger.type),
197
+ "progress_display": logger_config.get(
198
+ "progress_display", default_settings.logger.progress_display
199
+ ),
200
+ "show_chat": logger_config.get("show_chat", default_settings.logger.show_chat),
201
+ "show_tools": logger_config.get("show_tools", default_settings.logger.show_tools),
202
+ "truncate_tools": logger_config.get(
203
+ "truncate_tools", default_settings.logger.truncate_tools
204
+ ),
205
+ "enable_markup": logger_config.get(
206
+ "enable_markup", default_settings.logger.enable_markup
207
+ ),
215
208
  }
216
209
 
217
210
  # Get MCP server info
@@ -228,10 +221,11 @@ def get_config_summary(config_path: Optional[Path]) -> dict:
228
221
  if "url" in server_config:
229
222
  url = server_config.get("url", "")
230
223
  server_info["url"] = url
231
-
224
+
232
225
  # Use URL path to determine transport type
233
226
  try:
234
227
  from .url_parser import parse_server_url
228
+
235
229
  _, transport_type, _ = parse_server_url(url)
236
230
  server_info["transport"] = transport_type.upper()
237
231
  except Exception:
@@ -288,7 +282,9 @@ def show_check_summary() -> None:
288
282
  system_table.add_row("Python Version", ".".join(system_info["python_version"].split(".")[:3]))
289
283
  system_table.add_row("Python Path", system_info["python_path"])
290
284
 
291
- console.print(Panel(system_table, title="System Information", border_style="blue"))
285
+ console.print(
286
+ Panel(system_table, title="System Information", title_align="left", border_style="blue")
287
+ )
292
288
 
293
289
  # Configuration files panel
294
290
  config_path = config_files["config"]
@@ -327,25 +323,59 @@ def show_check_summary() -> None:
327
323
  "Default Model", config_summary.get("default_model", "haiku (system default)")
328
324
  )
329
325
 
330
- # Add logger settings if available
331
- logger = config_summary.get("logger", {})
332
- if logger:
333
- files_table.add_row("Logger Level", logger.get("level", "info (default)"))
334
- files_table.add_row("Logger Type", logger.get("type", "console (default)"))
335
- files_table.add_row("Progress Display", logger.get("progress_display", "True"))
336
- files_table.add_row("Show Chat", logger.get("show_chat", "True"))
337
- files_table.add_row("Show Tools", logger.get("show_tools", "True"))
326
+ console.print(
327
+ Panel(files_table, title="Configuration Files", title_align="left", border_style="blue")
328
+ )
329
+
330
+ # Logger Settings panel with two-column layout
331
+ logger = config_summary.get("logger", {})
332
+ logger_table = Table(show_header=True, box=None)
333
+ logger_table.add_column("Setting", style="cyan")
334
+ logger_table.add_column("Value")
335
+ logger_table.add_column("Setting", style="cyan")
336
+ logger_table.add_column("Value")
337
+
338
+ def bool_to_symbol(value):
339
+ return "[bold green]✓[/bold green]" if value else "[bold red]✗[/bold red]"
340
+
341
+ # Prepare all settings as pairs
342
+ settings_data = [
343
+ ("Logger Level", logger.get("level", "warning (default)")),
344
+ ("Logger Type", logger.get("type", "file (default)")),
345
+ ("Progress Display", bool_to_symbol(logger.get("progress_display", True))),
346
+ ("Show Chat", bool_to_symbol(logger.get("show_chat", True))),
347
+ ("Show Tools", bool_to_symbol(logger.get("show_tools", True))),
348
+ ("Truncate Tools", bool_to_symbol(logger.get("truncate_tools", True))),
349
+ ("Enable Markup", bool_to_symbol(logger.get("enable_markup", True))),
350
+ ]
351
+
352
+ # Add rows in two-column layout
353
+ for i in range(0, len(settings_data), 2):
354
+ left_setting, left_value = settings_data[i]
355
+ if i + 1 < len(settings_data):
356
+ right_setting, right_value = settings_data[i + 1]
357
+ logger_table.add_row(left_setting, left_value, right_setting, right_value)
358
+ else:
359
+ # Odd number of settings - fill right column with empty strings
360
+ logger_table.add_row(left_setting, left_value, "", "")
338
361
 
339
- console.print(Panel(files_table, title="Configuration Files", border_style="blue"))
362
+ console.print(
363
+ Panel(logger_table, title="Logger Settings", title_align="left", border_style="blue")
364
+ )
340
365
 
341
- # API keys panel
366
+ # API keys panel with two-column layout
342
367
  keys_table = Table(show_header=True, box=None)
343
368
  keys_table.add_column("Provider", style="cyan")
344
369
  keys_table.add_column("Env", justify="center")
345
370
  keys_table.add_column("Config", justify="center")
346
371
  keys_table.add_column("Active Key", style="green")
372
+ keys_table.add_column("Provider", style="cyan")
373
+ keys_table.add_column("Env", justify="center")
374
+ keys_table.add_column("Config", justify="center")
375
+ keys_table.add_column("Active Key", style="green")
347
376
 
348
- for provider, status in api_keys.items():
377
+ def format_provider_row(provider, status):
378
+ """Format a single provider's status for display."""
349
379
  # Environment key indicator
350
380
  if status["env"] and status["config"]:
351
381
  # Both exist but config takes precedence (env is present but not active)
@@ -379,10 +409,35 @@ def show_check_summary() -> None:
379
409
  # No key available for other providers
380
410
  active = "[dim]Not configured[/dim]"
381
411
 
382
- keys_table.add_row(provider.capitalize(), env_status, config_status, active)
412
+ # Get the proper display name for the provider
413
+ from mcp_agent.llm.provider_types import Provider
414
+
415
+ provider_enum = Provider(provider)
416
+ display_name = provider_enum.display_name
417
+
418
+ return display_name, env_status, config_status, active
419
+
420
+ # Split providers into two columns
421
+ providers_list = list(api_keys.items())
422
+ mid_point = (len(providers_list) + 1) // 2 # Round up for odd numbers
423
+
424
+ for i in range(mid_point):
425
+ # Left column
426
+ left_provider, left_status = providers_list[i]
427
+ left_data = format_provider_row(left_provider, left_status)
428
+
429
+ # Right column (if exists)
430
+ if i + mid_point < len(providers_list):
431
+ right_provider, right_status = providers_list[i + mid_point]
432
+ right_data = format_provider_row(right_provider, right_status)
433
+ # Add row with both columns
434
+ keys_table.add_row(*left_data, *right_data)
435
+ else:
436
+ # Add row with only left column (right column empty)
437
+ keys_table.add_row(*left_data, "", "", "", "")
383
438
 
384
439
  # Print the API Keys panel (fix: this was missing)
385
- keys_panel = Panel(keys_table, title="API Keys", border_style="blue", subtitle_align="left")
440
+ keys_panel = Panel(keys_table, title="API Keys", title_align="left", border_style="blue")
386
441
  console.print(keys_panel)
387
442
 
388
443
  # MCP Servers panel (shown after API Keys)
@@ -406,12 +461,16 @@ def show_check_summary() -> None:
406
461
  command_url = server["url"] or "[dim]Not configured[/dim]"
407
462
  servers_table.add_row(name, transport, command_url)
408
463
 
409
- console.print(Panel(servers_table, title="MCP Servers", border_style="blue"))
464
+ console.print(
465
+ Panel(servers_table, title="MCP Servers", title_align="left", border_style="blue")
466
+ )
410
467
 
411
468
  # Show help tips
412
469
  if config_status == "not_found" or secrets_status == "not_found":
413
470
  console.print("\n[bold]Setup Tips:[/bold]")
414
- console.print("Run [cyan]fast-agent setup[/cyan] to create configuration files")
471
+ console.print(
472
+ "Run [cyan]fast-agent setup[/cyan] to create configuration files. Visit [cyan][link=https://fast-agent.ai]fast-agent.ai[/link][/cyan] for configuration guides. "
473
+ )
415
474
  elif config_status == "error" or secrets_status == "error":
416
475
  console.print("\n[bold]Config File Issues:[/bold]")
417
476
  console.print("Fix the YAML syntax errors in your configuration files")
@@ -1,16 +1,20 @@
1
1
  """Run an interactive agent directly from the command line."""
2
2
 
3
3
  import asyncio
4
+ import shlex
4
5
  import sys
5
6
  from typing import Dict, List, Optional
6
7
 
7
8
  import typer
8
9
 
10
+ from mcp_agent.cli.commands.server_helpers import add_servers_to_config, generate_server_name
9
11
  from mcp_agent.cli.commands.url_parser import generate_server_configs, parse_server_urls
10
12
  from mcp_agent.core.fastagent import FastAgent
13
+ from mcp_agent.ui.console_display import ConsoleDisplay
11
14
 
12
15
  app = typer.Typer(
13
- help="Run an interactive agent directly from the command line without creating an agent.py file"
16
+ help="Run an interactive agent directly from the command line without creating an agent.py file",
17
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
14
18
  )
15
19
 
16
20
 
@@ -23,11 +27,11 @@ async def _run_agent(
23
27
  message: Optional[str] = None,
24
28
  prompt_file: Optional[str] = None,
25
29
  url_servers: Optional[Dict[str, Dict[str, str]]] = None,
30
+ stdio_servers: Optional[Dict[str, Dict[str, str]]] = None,
26
31
  ) -> None:
27
32
  """Async implementation to run an interactive agent."""
28
33
  from pathlib import Path
29
34
 
30
- from mcp_agent.config import MCPServerSettings, MCPSettings
31
35
  from mcp_agent.mcp.prompts.prompt_load import load_prompt_multipart
32
36
 
33
37
  # Create the FastAgent instance
@@ -40,41 +44,69 @@ async def _run_agent(
40
44
 
41
45
  fast = FastAgent(**fast_kwargs)
42
46
 
43
- # Add URL-based servers to the context configuration
44
- if url_servers:
45
- # Initialize the app to ensure context is ready
46
- await fast.app.initialize()
47
+ # Add all dynamic servers to the configuration
48
+ await add_servers_to_config(fast, url_servers)
49
+ await add_servers_to_config(fast, stdio_servers)
47
50
 
48
- # Initialize mcp settings if needed
49
- if not hasattr(fast.app.context.config, "mcp"):
50
- fast.app.context.config.mcp = MCPSettings()
51
+ # Check if we have multiple models (comma-delimited)
52
+ if model and "," in model:
53
+ # Parse multiple models
54
+ models = [m.strip() for m in model.split(",") if m.strip()]
51
55
 
52
- # Initialize servers dictionary if needed
53
- if (
54
- not hasattr(fast.app.context.config.mcp, "servers")
55
- or fast.app.context.config.mcp.servers is None
56
- ):
57
- fast.app.context.config.mcp.servers = {}
56
+ # Create an agent for each model
57
+ fan_out_agents = []
58
+ for i, model_name in enumerate(models):
59
+ agent_name = f"{model_name}"
58
60
 
59
- # Add each URL server to the config
60
- for server_name, server_config in url_servers.items():
61
- server_settings = {"transport": server_config["transport"], "url": server_config["url"]}
61
+ # Define the agent with specified parameters
62
+ agent_kwargs = {"instruction": instruction, "name": agent_name}
63
+ if server_list:
64
+ agent_kwargs["servers"] = server_list
65
+ agent_kwargs["model"] = model_name
62
66
 
63
- # Add headers if present in the server config
64
- if "headers" in server_config:
65
- server_settings["headers"] = server_config["headers"]
67
+ @fast.agent(**agent_kwargs)
68
+ async def model_agent():
69
+ pass
66
70
 
67
- fast.app.context.config.mcp.servers[server_name] = MCPServerSettings(**server_settings)
71
+ fan_out_agents.append(agent_name)
68
72
 
69
- # Define the agent with specified parameters
70
- agent_kwargs = {"instruction": instruction}
71
- if server_list:
72
- agent_kwargs["servers"] = server_list
73
- if model:
74
- agent_kwargs["model"] = model
73
+ # Create a silent fan-in agent for cleaner output
74
+ @fast.agent(
75
+ name="aggregate",
76
+ model="silent",
77
+ instruction="You are a silent agent that combines outputs from parallel agents.",
78
+ )
79
+ async def fan_in_agent():
80
+ pass
75
81
 
76
- # Handle prompt file and message options
77
- if message or prompt_file:
82
+ # Create a parallel agent with silent fan_in
83
+ @fast.parallel(
84
+ name="parallel",
85
+ fan_out=fan_out_agents,
86
+ fan_in="aggregate",
87
+ include_request=True,
88
+ )
89
+ async def cli_agent():
90
+ async with fast.run() as agent:
91
+ if message:
92
+ await agent.parallel.send(message)
93
+ display = ConsoleDisplay(config=None)
94
+ display.show_parallel_results(agent.parallel)
95
+ elif prompt_file:
96
+ prompt = load_prompt_multipart(Path(prompt_file))
97
+ await agent.parallel.generate(prompt)
98
+ display = ConsoleDisplay(config=None)
99
+ display.show_parallel_results(agent.parallel)
100
+ else:
101
+ await agent.interactive(agent_name="parallel", pretty_print_parallel=True)
102
+ else:
103
+ # Single model - use original behavior
104
+ # Define the agent with specified parameters
105
+ agent_kwargs = {"instruction": instruction}
106
+ if server_list:
107
+ agent_kwargs["servers"] = server_list
108
+ if model:
109
+ agent_kwargs["model"] = model
78
110
 
79
111
  @fast.agent(**agent_kwargs)
80
112
  async def cli_agent():
@@ -88,12 +120,8 @@ async def _run_agent(
88
120
  response = await agent.default.generate(prompt)
89
121
  # Print the response text and exit
90
122
  print(response.last_text())
91
- else:
92
- # Standard interactive mode
93
- @fast.agent(**agent_kwargs)
94
- async def cli_agent():
95
- async with fast.run() as agent:
96
- await agent.interactive()
123
+ else:
124
+ await agent.interactive()
97
125
 
98
126
  # Run the agent
99
127
  await cli_agent()
@@ -109,6 +137,7 @@ def run_async_agent(
109
137
  model: Optional[str] = None,
110
138
  message: Optional[str] = None,
111
139
  prompt_file: Optional[str] = None,
140
+ stdio_commands: Optional[List[str]] = None,
112
141
  ):
113
142
  """Run the async agent function with proper loop handling."""
114
143
  server_list = servers.split(",") if servers else None
@@ -129,6 +158,60 @@ def run_async_agent(
129
158
  print(f"Error parsing URLs: {e}")
130
159
  return
131
160
 
161
+ # Generate STDIO server configurations if provided
162
+ stdio_servers = None
163
+
164
+ if stdio_commands:
165
+ stdio_servers = {}
166
+ for i, stdio_cmd in enumerate(stdio_commands):
167
+ # Parse the stdio command string
168
+ try:
169
+ parsed_command = shlex.split(stdio_cmd)
170
+ if not parsed_command:
171
+ print(f"Error: Empty stdio command: {stdio_cmd}")
172
+ continue
173
+
174
+ command = parsed_command[0]
175
+ initial_args = parsed_command[1:] if len(parsed_command) > 1 else []
176
+
177
+ # Generate a server name from the command
178
+ if initial_args:
179
+ # Try to extract a meaningful name from the args
180
+ for arg in initial_args:
181
+ if arg.endswith(".py") or arg.endswith(".js") or arg.endswith(".ts"):
182
+ base_name = generate_server_name(arg)
183
+ break
184
+ else:
185
+ # Fallback to command name
186
+ base_name = generate_server_name(command)
187
+ else:
188
+ base_name = generate_server_name(command)
189
+
190
+ # Ensure unique server names when multiple servers
191
+ server_name = base_name
192
+ if len(stdio_commands) > 1:
193
+ server_name = f"{base_name}_{i + 1}"
194
+
195
+ # Build the complete args list
196
+ stdio_command_args = initial_args.copy()
197
+
198
+ # Add this server to the configuration
199
+ stdio_servers[server_name] = {
200
+ "transport": "stdio",
201
+ "command": command,
202
+ "args": stdio_command_args,
203
+ }
204
+
205
+ # Add STDIO server to the server list
206
+ if not server_list:
207
+ server_list = [server_name]
208
+ else:
209
+ server_list.append(server_name)
210
+
211
+ except ValueError as e:
212
+ print(f"Error parsing stdio command '{stdio_cmd}': {e}")
213
+ continue
214
+
132
215
  # Check if we're already in an event loop
133
216
  try:
134
217
  loop = asyncio.get_event_loop()
@@ -153,6 +236,7 @@ def run_async_agent(
153
236
  message=message,
154
237
  prompt_file=prompt_file,
155
238
  url_servers=url_servers,
239
+ stdio_servers=stdio_servers,
156
240
  )
157
241
  )
158
242
  finally:
@@ -171,7 +255,7 @@ def run_async_agent(
171
255
  pass
172
256
 
173
257
 
174
- @app.callback(invoke_without_command=True)
258
+ @app.callback(invoke_without_command=True, no_args_is_help=False)
175
259
  def go(
176
260
  ctx: typer.Context,
177
261
  name: str = typer.Option("FastAgent CLI", "--name", help="Name for the agent"),
@@ -191,7 +275,7 @@ def go(
191
275
  None, "--auth", help="Bearer token for authorization with URL-based servers"
192
276
  ),
193
277
  model: Optional[str] = typer.Option(
194
- None, "--model", help="Override the default model (e.g., haiku, sonnet, gpt-4)"
278
+ None, "--model", "--models", help="Override the default model (e.g., haiku, sonnet, gpt-4)"
195
279
  ),
196
280
  message: Optional[str] = typer.Option(
197
281
  None, "--message", "-m", help="Message to send to the agent (skips interactive mode)"
@@ -199,6 +283,15 @@ def go(
199
283
  prompt_file: Optional[str] = typer.Option(
200
284
  None, "--prompt-file", "-p", help="Path to a prompt file to use (either text or JSON)"
201
285
  ),
286
+ npx: Optional[str] = typer.Option(
287
+ None, "--npx", help="NPX package and args to run as MCP server (quoted)"
288
+ ),
289
+ uvx: Optional[str] = typer.Option(
290
+ None, "--uvx", help="UVX package and args to run as MCP server (quoted)"
291
+ ),
292
+ stdio: Optional[str] = typer.Option(
293
+ None, "--stdio", help="Command to run as STDIO MCP server (quoted)"
294
+ ),
202
295
  ) -> None:
203
296
  """
204
297
  Run an interactive agent directly from the command line.
@@ -209,6 +302,10 @@ def go(
209
302
  fast-agent go --prompt-file=my-prompt.txt --model=haiku
210
303
  fast-agent go --url=http://localhost:8001/mcp,http://api.example.com/sse
211
304
  fast-agent go --url=https://api.example.com/mcp --auth=YOUR_API_TOKEN
305
+ fast-agent go --npx "@modelcontextprotocol/server-filesystem /path/to/data"
306
+ fast-agent go --uvx "mcp-server-fetch --verbose"
307
+ fast-agent go --stdio "python my_server.py --debug"
308
+ fast-agent go --stdio "uv run server.py --config=settings.json"
212
309
 
213
310
  This will start an interactive session with the agent, using the specified model
214
311
  and instruction. It will use the default configuration from fastagent.config.yaml
@@ -222,7 +319,22 @@ def go(
222
319
  --auth Bearer token for authorization with URL-based servers
223
320
  --message, -m Send a single message and exit
224
321
  --prompt-file, -p Use a prompt file instead of interactive mode
322
+ --npx NPX package and args to run as MCP server (quoted)
323
+ --uvx UVX package and args to run as MCP server (quoted)
324
+ --stdio Command to run as STDIO MCP server (quoted)
225
325
  """
326
+ # Collect all stdio commands from convenience options
327
+ stdio_commands = []
328
+
329
+ if npx:
330
+ stdio_commands.append(f"npx {npx}")
331
+
332
+ if uvx:
333
+ stdio_commands.append(f"uvx {uvx}")
334
+
335
+ if stdio:
336
+ stdio_commands.append(stdio)
337
+
226
338
  run_async_agent(
227
339
  name=name,
228
340
  instruction=instruction,
@@ -233,4 +345,5 @@ def go(
233
345
  model=model,
234
346
  message=message,
235
347
  prompt_file=prompt_file,
348
+ stdio_commands=stdio_commands,
236
349
  )
@@ -383,12 +383,16 @@ def _show_completion_message(example_type: str, created: list[str]) -> None:
383
383
  "On Windows platforms, please edit the fastagent.config.yaml and adjust the volume mount point."
384
384
  )
385
385
  elif example_type == "state-transfer":
386
- console.print("Check https://fast-agent.ai for quick start walkthroughs")
386
+ console.print(
387
+ "Check [cyan][link=https://fast-agent.ai]fast-agent.ai[/link][/cyan] for quick start walkthroughs"
388
+ )
387
389
  elif example_type == "elicitations":
388
390
  console.print("1. Go to the `elicitations` subdirectory (cd elicitations)")
389
391
  console.print("2. Try the forms demo: uv run forms_demo.py")
390
392
  console.print("3. Run the game character creator: uv run game_character.py")
391
- console.print("Check https://fast-agent.ai/mcp/elicitations/ for more details")
393
+ console.print(
394
+ "Check [cyan][link=https://fast-agent.ai/mcp/elicitations/]https://fast-agent.ai/mcp/elicitations/[/link][/cyan] for more details"
395
+ )
392
396
  else:
393
397
  console.print("\n[yellow]No files were created.[/yellow]")
394
398