fast-agent-mcp 0.2.40__py3-none-any.whl → 0.2.41__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 (41) hide show
  1. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.41.dist-info}/METADATA +1 -1
  2. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.41.dist-info}/RECORD +41 -37
  3. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.41.dist-info}/entry_points.txt +2 -2
  4. mcp_agent/cli/__main__.py +29 -3
  5. mcp_agent/cli/commands/check_config.py +140 -81
  6. mcp_agent/cli/commands/go.py +151 -38
  7. mcp_agent/cli/commands/quickstart.py +6 -2
  8. mcp_agent/cli/commands/server_helpers.py +106 -0
  9. mcp_agent/cli/constants.py +25 -0
  10. mcp_agent/cli/main.py +1 -1
  11. mcp_agent/config.py +94 -44
  12. mcp_agent/core/agent_app.py +104 -15
  13. mcp_agent/core/agent_types.py +1 -0
  14. mcp_agent/core/direct_decorators.py +9 -0
  15. mcp_agent/core/direct_factory.py +18 -4
  16. mcp_agent/core/enhanced_prompt.py +165 -13
  17. mcp_agent/core/fastagent.py +4 -0
  18. mcp_agent/core/interactive_prompt.py +37 -37
  19. mcp_agent/core/usage_display.py +11 -1
  20. mcp_agent/core/validation.py +21 -2
  21. mcp_agent/human_input/elicitation_form.py +53 -21
  22. mcp_agent/llm/augmented_llm.py +28 -9
  23. mcp_agent/llm/augmented_llm_silent.py +48 -0
  24. mcp_agent/llm/model_database.py +20 -0
  25. mcp_agent/llm/model_factory.py +12 -0
  26. mcp_agent/llm/provider_key_manager.py +22 -8
  27. mcp_agent/llm/provider_types.py +19 -12
  28. mcp_agent/llm/providers/augmented_llm_anthropic.py +7 -2
  29. mcp_agent/llm/providers/augmented_llm_azure.py +7 -1
  30. mcp_agent/llm/providers/augmented_llm_google_native.py +4 -1
  31. mcp_agent/llm/providers/augmented_llm_openai.py +9 -2
  32. mcp_agent/llm/providers/augmented_llm_xai.py +38 -0
  33. mcp_agent/llm/usage_tracking.py +28 -3
  34. mcp_agent/mcp/mcp_agent_client_session.py +2 -0
  35. mcp_agent/mcp/mcp_aggregator.py +38 -44
  36. mcp_agent/mcp/sampling.py +15 -11
  37. mcp_agent/resources/examples/mcp/elicitations/forms_demo.py +0 -6
  38. mcp_agent/resources/examples/workflows/router.py +9 -0
  39. mcp_agent/ui/console_display.py +125 -13
  40. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.41.dist-info}/WHEEL +0 -0
  41. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.41.dist-info}/licenses/LICENSE +0 -0
@@ -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
 
@@ -0,0 +1,106 @@
1
+ """Helper functions for server configuration and naming."""
2
+
3
+ from typing import Any, Dict
4
+
5
+
6
+ def generate_server_name(identifier: str) -> str:
7
+ """Generate a clean server name from various identifiers.
8
+
9
+ Args:
10
+ identifier: Package name, file path, or other identifier
11
+
12
+ Returns:
13
+ Clean server name with only alphanumeric and underscore characters
14
+
15
+ Examples:
16
+ >>> generate_server_name("@modelcontextprotocol/server-filesystem")
17
+ 'server_filesystem'
18
+ >>> generate_server_name("./src/my-server.py")
19
+ 'src_my_server'
20
+ >>> generate_server_name("my-mcp-server")
21
+ 'my_mcp_server'
22
+ """
23
+
24
+ # Remove leading ./ if present
25
+ if identifier.startswith("./"):
26
+ identifier = identifier[2:]
27
+
28
+ # Handle npm package names with org prefix (only if no file extension)
29
+ has_file_ext = any(identifier.endswith(ext) for ext in [".py", ".js", ".ts"])
30
+ if "/" in identifier and not has_file_ext:
31
+ # This is likely an npm package, take the part after the last slash
32
+ identifier = identifier.split("/")[-1]
33
+
34
+ # Remove file extension for common script files
35
+ for ext in [".py", ".js", ".ts"]:
36
+ if identifier.endswith(ext):
37
+ identifier = identifier[: -len(ext)]
38
+ break
39
+
40
+ # Replace special characters with underscores
41
+ # Remove @ prefix if present
42
+ identifier = identifier.lstrip("@")
43
+
44
+ # Replace non-alphanumeric characters with underscores
45
+ server_name = ""
46
+ for char in identifier:
47
+ if char.isalnum():
48
+ server_name += char
49
+ else:
50
+ server_name += "_"
51
+
52
+ # Clean up multiple underscores
53
+ while "__" in server_name:
54
+ server_name = server_name.replace("__", "_")
55
+
56
+ # Remove leading/trailing underscores
57
+ server_name = server_name.strip("_")
58
+
59
+ return server_name
60
+
61
+
62
+ async def add_servers_to_config(fast_app: Any, servers: Dict[str, Dict[str, Any]]) -> None:
63
+ """Add server configurations to the FastAgent app config.
64
+
65
+ This function handles the repetitive initialization and configuration
66
+ of MCP servers, ensuring the app is initialized and the config
67
+ structure exists before adding servers.
68
+
69
+ Args:
70
+ fast_app: The FastAgent instance
71
+ servers: Dictionary of server configurations
72
+ """
73
+ if not servers:
74
+ return
75
+
76
+ from mcp_agent.config import MCPServerSettings, MCPSettings
77
+
78
+ # Initialize the app to ensure context is ready
79
+ await fast_app.app.initialize()
80
+
81
+ # Initialize mcp settings if needed
82
+ if not hasattr(fast_app.app.context.config, "mcp"):
83
+ fast_app.app.context.config.mcp = MCPSettings()
84
+
85
+ # Initialize servers dictionary if needed
86
+ if (
87
+ not hasattr(fast_app.app.context.config.mcp, "servers")
88
+ or fast_app.app.context.config.mcp.servers is None
89
+ ):
90
+ fast_app.app.context.config.mcp.servers = {}
91
+
92
+ # Add each server to the config
93
+ for server_name, server_config in servers.items():
94
+ # Build server settings based on transport type
95
+ server_settings = {"transport": server_config["transport"]}
96
+
97
+ # Add transport-specific settings
98
+ if server_config["transport"] == "stdio":
99
+ server_settings["command"] = server_config["command"]
100
+ server_settings["args"] = server_config["args"]
101
+ elif server_config["transport"] in ["http", "sse"]:
102
+ server_settings["url"] = server_config["url"]
103
+ if "headers" in server_config:
104
+ server_settings["headers"] = server_config["headers"]
105
+
106
+ fast_app.app.context.config.mcp.servers[server_name] = MCPServerSettings(**server_settings)
@@ -0,0 +1,25 @@
1
+ """Shared constants for CLI routing and commands."""
2
+
3
+ # Options that should automatically route to the 'go' command
4
+ GO_SPECIFIC_OPTIONS = {
5
+ "--npx",
6
+ "--uvx",
7
+ "--stdio",
8
+ "--url",
9
+ "--model",
10
+ "--models",
11
+ "--instruction",
12
+ "-i",
13
+ "--message",
14
+ "-m",
15
+ "--prompt-file",
16
+ "-p",
17
+ "--servers",
18
+ "--auth",
19
+ "--name",
20
+ "--config-path",
21
+ "-c",
22
+ }
23
+
24
+ # Known subcommands that should not trigger auto-routing
25
+ KNOWN_SUBCOMMANDS = {"go", "setup", "check", "bootstrap", "quickstart", "--help", "-h", "--version"}
mcp_agent/cli/main.py CHANGED
@@ -48,7 +48,7 @@ def show_welcome() -> None:
48
48
  console.print(table)
49
49
 
50
50
  console.print(
51
- "\n[italic]get started with:[/italic] [bold][cyan]fast-agent[/cyan][/bold] [green]setup[/green]"
51
+ "\n[italic]get started with:[/italic] [bold][cyan]fast-agent[/cyan][/bold] [green]setup[/green]. visit [cyan][link=https://fast-agent.ai]fast-agent.ai[/link][/cyan] for more information."
52
52
  )
53
53
 
54
54
 
mcp_agent/config.py CHANGED
@@ -6,7 +6,7 @@ for the application configuration.
6
6
  import os
7
7
  import re
8
8
  from pathlib import Path
9
- from typing import Any, Dict, List, Literal, Optional
9
+ from typing import Any, Dict, List, Literal, Optional, Tuple
10
10
 
11
11
  from pydantic import BaseModel, ConfigDict, field_validator
12
12
  from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -175,6 +175,17 @@ class GoogleSettings(BaseModel):
175
175
  model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
176
176
 
177
177
 
178
+ class XAISettings(BaseModel):
179
+ """
180
+ Settings for using xAI Grok models in the fast-agent application.
181
+ """
182
+
183
+ api_key: str | None = None
184
+ base_url: str | None = "https://api.x.ai/v1"
185
+
186
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
187
+
188
+
178
189
  class GenericSettings(BaseModel):
179
190
  """
180
191
  Settings for using OpenAI models in the fast-agent application.
@@ -296,6 +307,57 @@ class LoggerSettings(BaseModel):
296
307
  """Enable markup in console output. Disable for outputs that may conflict with rich console formatting"""
297
308
 
298
309
 
310
+ def find_fastagent_config_files(start_path: Path) -> Tuple[Optional[Path], Optional[Path]]:
311
+ """
312
+ Find FastAgent configuration files with standardized behavior.
313
+
314
+ Returns:
315
+ Tuple of (config_path, secrets_path) where either can be None if not found.
316
+
317
+ Strategy:
318
+ 1. Find config file recursively from start_path upward
319
+ 2. Prefer secrets file in same directory as config file
320
+ 3. If no secrets file next to config, search recursively from start_path
321
+ """
322
+ config_path = None
323
+ secrets_path = None
324
+
325
+ # First, find the config file with recursive search
326
+ current = start_path.resolve()
327
+ while current != current.parent:
328
+ potential_config = current / "fastagent.config.yaml"
329
+ if potential_config.exists():
330
+ config_path = potential_config
331
+ break
332
+ current = current.parent
333
+
334
+ # If config file found, prefer secrets file in the same directory
335
+ if config_path:
336
+ potential_secrets = config_path.parent / "fastagent.secrets.yaml"
337
+ if potential_secrets.exists():
338
+ secrets_path = potential_secrets
339
+ else:
340
+ # If no secrets file next to config, do recursive search from start
341
+ current = start_path.resolve()
342
+ while current != current.parent:
343
+ potential_secrets = current / "fastagent.secrets.yaml"
344
+ if potential_secrets.exists():
345
+ secrets_path = potential_secrets
346
+ break
347
+ current = current.parent
348
+ else:
349
+ # No config file found, just search for secrets file
350
+ current = start_path.resolve()
351
+ while current != current.parent:
352
+ potential_secrets = current / "fastagent.secrets.yaml"
353
+ if potential_secrets.exists():
354
+ secrets_path = potential_secrets
355
+ break
356
+ current = current.parent
357
+
358
+ return config_path, secrets_path
359
+
360
+
299
361
  class Settings(BaseSettings):
300
362
  """
301
363
  Settings class for the fast-agent application.
@@ -339,6 +401,9 @@ class Settings(BaseSettings):
339
401
  google: GoogleSettings | None = None
340
402
  """Settings for using DeepSeek models in the fast-agent application"""
341
403
 
404
+ xai: XAISettings | None = None
405
+ """Settings for using xAI Grok models in the fast-agent application"""
406
+
342
407
  openrouter: OpenRouterSettings | None = None
343
408
  """Settings for using OpenRouter models in the fast-agent application"""
344
409
 
@@ -445,51 +510,36 @@ def get_settings(config_path: str | None = None) -> Settings:
445
510
  resolved_path = Path.cwd() / config_file.name
446
511
  if resolved_path.exists():
447
512
  config_file = resolved_path
513
+
514
+ # When config path is explicitly provided, find secrets using standardized logic
515
+ secrets_file = None
516
+ if config_file.exists():
517
+ _, secrets_file = find_fastagent_config_files(config_file.parent)
448
518
  else:
449
- config_file = Settings.find_config()
519
+ # Use standardized discovery for both config and secrets
520
+ config_file, secrets_file = find_fastagent_config_files(Path.cwd())
450
521
 
451
522
  merged_settings = {}
452
523
 
453
- if config_file:
454
- if not config_file.exists():
455
- print(f"Warning: Specified config file does not exist: {config_file}")
456
- else:
457
- import yaml # pylint: disable=C0415
458
-
459
- # Load main config
460
- with open(config_file, "r", encoding="utf-8") as f:
461
- yaml_settings = yaml.safe_load(f) or {}
462
- # Resolve environment variables in the loaded YAML settings
463
- resolved_yaml_settings = resolve_env_vars(yaml_settings)
464
- merged_settings = resolved_yaml_settings
465
- # Look for secrets files recursively up the directory tree
466
- # but stop after finding the first one
467
- current_dir = config_file.parent
468
- found_secrets = False
469
- # Start with the absolute path of the config file\'s directory
470
- current_dir = config_file.parent.resolve()
471
-
472
- while current_dir != current_dir.parent and not found_secrets:
473
- for secrets_filename in [
474
- "fastagent.secrets.yaml",
475
- ]:
476
- secrets_file = current_dir / secrets_filename
477
- if secrets_file.exists():
478
- with open(secrets_file, "r", encoding="utf-8") as f:
479
- yaml_secrets = yaml.safe_load(f) or {}
480
- # Resolve environment variables in the loaded secrets YAML
481
- resolved_secrets_yaml = resolve_env_vars(yaml_secrets)
482
- merged_settings = deep_merge(merged_settings, resolved_secrets_yaml)
483
- found_secrets = True
484
- break
485
- if not found_secrets:
486
- # Get the absolute path of the parent directory
487
- current_dir = current_dir.parent.resolve()
488
-
489
- _settings = Settings(**merged_settings)
490
- return _settings
491
- else:
492
- pass
493
-
494
- _settings = Settings()
524
+ import yaml # pylint: disable=C0415
525
+
526
+ # Load main config if it exists
527
+ if config_file and config_file.exists():
528
+ with open(config_file, "r", encoding="utf-8") as f:
529
+ yaml_settings = yaml.safe_load(f) or {}
530
+ # Resolve environment variables in the loaded YAML settings
531
+ resolved_yaml_settings = resolve_env_vars(yaml_settings)
532
+ merged_settings = resolved_yaml_settings
533
+ elif config_file and not config_file.exists():
534
+ print(f"Warning: Specified config file does not exist: {config_file}")
535
+
536
+ # Load secrets file if found (regardless of whether config file exists)
537
+ if secrets_file and secrets_file.exists():
538
+ with open(secrets_file, "r", encoding="utf-8") as f:
539
+ yaml_secrets = yaml.safe_load(f) or {}
540
+ # Resolve environment variables in the loaded secrets YAML
541
+ resolved_secrets_yaml = resolve_env_vars(yaml_secrets)
542
+ merged_settings = deep_merge(merged_settings, resolved_secrets_yaml)
543
+
544
+ _settings = Settings(**merged_settings)
495
545
  return _settings