fast-agent-mcp 0.2.39__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.
- {fast_agent_mcp-0.2.39.dist-info → fast_agent_mcp-0.2.41.dist-info}/METADATA +1 -1
- {fast_agent_mcp-0.2.39.dist-info → fast_agent_mcp-0.2.41.dist-info}/RECORD +41 -37
- {fast_agent_mcp-0.2.39.dist-info → fast_agent_mcp-0.2.41.dist-info}/entry_points.txt +2 -2
- mcp_agent/cli/__main__.py +29 -3
- mcp_agent/cli/commands/check_config.py +140 -81
- mcp_agent/cli/commands/go.py +151 -38
- mcp_agent/cli/commands/quickstart.py +8 -4
- mcp_agent/cli/commands/server_helpers.py +106 -0
- mcp_agent/cli/constants.py +25 -0
- mcp_agent/cli/main.py +1 -1
- mcp_agent/config.py +94 -44
- mcp_agent/core/agent_app.py +104 -15
- mcp_agent/core/agent_types.py +1 -0
- mcp_agent/core/direct_decorators.py +9 -0
- mcp_agent/core/direct_factory.py +18 -4
- mcp_agent/core/enhanced_prompt.py +165 -13
- mcp_agent/core/fastagent.py +4 -0
- mcp_agent/core/interactive_prompt.py +37 -37
- mcp_agent/core/usage_display.py +11 -1
- mcp_agent/core/validation.py +21 -2
- mcp_agent/human_input/elicitation_form.py +55 -22
- mcp_agent/llm/augmented_llm.py +28 -9
- mcp_agent/llm/augmented_llm_silent.py +48 -0
- mcp_agent/llm/model_database.py +20 -0
- mcp_agent/llm/model_factory.py +12 -0
- mcp_agent/llm/provider_key_manager.py +22 -8
- mcp_agent/llm/provider_types.py +19 -12
- mcp_agent/llm/providers/augmented_llm_anthropic.py +7 -2
- mcp_agent/llm/providers/augmented_llm_azure.py +7 -1
- mcp_agent/llm/providers/augmented_llm_google_native.py +4 -1
- mcp_agent/llm/providers/augmented_llm_openai.py +9 -2
- mcp_agent/llm/providers/augmented_llm_xai.py +38 -0
- mcp_agent/llm/usage_tracking.py +28 -3
- mcp_agent/mcp/mcp_agent_client_session.py +2 -0
- mcp_agent/mcp/mcp_aggregator.py +38 -44
- mcp_agent/mcp/sampling.py +15 -11
- mcp_agent/resources/examples/mcp/elicitations/forms_demo.py +0 -6
- mcp_agent/resources/examples/workflows/router.py +9 -0
- mcp_agent/ui/console_display.py +125 -13
- {fast_agent_mcp-0.2.39.dist-info → fast_agent_mcp-0.2.41.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.39.dist-info → fast_agent_mcp-0.2.41.dist-info}/licenses/LICENSE +0 -0
mcp_agent/cli/commands/go.py
CHANGED
|
@@ -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
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
@fast.agent(**agent_kwargs)
|
|
68
|
+
async def model_agent():
|
|
69
|
+
pass
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
fan_out_agents.append(agent_name)
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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(
|
|
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
|
-
console.print("1.
|
|
389
|
-
console.print("2.
|
|
390
|
+
console.print("1. Go to the `elicitations` subdirectory (cd elicitations)")
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
#
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
#
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|