fast-agent-mcp 0.3.4__py3-none-any.whl → 0.3.6__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/agents/llm_agent.py +15 -1
- fast_agent/agents/mcp_agent.py +73 -1
- fast_agent/agents/tool_agent.py +10 -0
- fast_agent/agents/workflow/router_agent.py +10 -2
- fast_agent/cli/__main__.py +8 -5
- fast_agent/cli/commands/auth.py +393 -0
- fast_agent/cli/commands/check_config.py +76 -4
- fast_agent/cli/commands/go.py +8 -2
- fast_agent/cli/commands/quickstart.py +3 -1
- fast_agent/cli/commands/server_helpers.py +10 -2
- fast_agent/cli/commands/setup.py +7 -9
- fast_agent/cli/constants.py +1 -1
- fast_agent/cli/main.py +3 -1
- fast_agent/config.py +63 -9
- fast_agent/mcp/mcp_aggregator.py +30 -0
- fast_agent/mcp/mcp_connection_manager.py +41 -4
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/resources/setup/.gitignore +6 -0
- fast_agent/resources/setup/agent.py +8 -1
- fast_agent/resources/setup/fastagent.config.yaml +1 -2
- fast_agent/resources/setup/pyproject.toml.tmpl +6 -0
- fast_agent/ui/console_display.py +48 -31
- fast_agent/ui/enhanced_prompt.py +8 -0
- fast_agent/ui/interactive_prompt.py +54 -0
- {fast_agent_mcp-0.3.4.dist-info → fast_agent_mcp-0.3.6.dist-info}/METADATA +39 -2
- {fast_agent_mcp-0.3.4.dist-info → fast_agent_mcp-0.3.6.dist-info}/RECORD +29 -27
- {fast_agent_mcp-0.3.4.dist-info → fast_agent_mcp-0.3.6.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.4.dist-info → fast_agent_mcp-0.3.6.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.4.dist-info → fast_agent_mcp-0.3.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,18 +8,17 @@ from typing import Optional
|
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
10
|
import yaml
|
|
11
|
-
from rich.console import Console
|
|
12
11
|
from rich.table import Table
|
|
13
12
|
from rich.text import Text
|
|
14
13
|
|
|
15
14
|
from fast_agent.llm.provider_key_manager import API_KEY_HINT_TEXT, ProviderKeyManager
|
|
16
15
|
from fast_agent.llm.provider_types import Provider
|
|
16
|
+
from fast_agent.ui.console import console
|
|
17
17
|
|
|
18
18
|
app = typer.Typer(
|
|
19
19
|
help="Check and diagnose FastAgent configuration",
|
|
20
20
|
no_args_is_help=False, # Allow showing our custom help instead
|
|
21
21
|
)
|
|
22
|
-
console = Console()
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
def find_config_files(start_path: Path) -> dict[str, Optional[Path]]:
|
|
@@ -305,6 +304,27 @@ def show_check_summary() -> None:
|
|
|
305
304
|
env_table.add_column("Setting", style="white")
|
|
306
305
|
env_table.add_column("Value")
|
|
307
306
|
|
|
307
|
+
# Determine keyring backend early so it can appear in the top section
|
|
308
|
+
# Also detect whether the backend is actually usable (not the fail backend)
|
|
309
|
+
keyring_usable = False
|
|
310
|
+
try:
|
|
311
|
+
import keyring # type: ignore
|
|
312
|
+
|
|
313
|
+
keyring_backend = keyring.get_keyring()
|
|
314
|
+
keyring_name = getattr(keyring_backend, "name", keyring_backend.__class__.__name__)
|
|
315
|
+
try:
|
|
316
|
+
# Detect the "fail" backend explicitly; it's present but unusable
|
|
317
|
+
from keyring.backends.fail import Keyring as FailKeyring # type: ignore
|
|
318
|
+
|
|
319
|
+
keyring_usable = not isinstance(keyring_backend, FailKeyring)
|
|
320
|
+
except Exception:
|
|
321
|
+
# If we can't import the fail backend marker, assume usable
|
|
322
|
+
keyring_usable = True
|
|
323
|
+
except Exception:
|
|
324
|
+
keyring = None # type: ignore
|
|
325
|
+
keyring_name = "unavailable"
|
|
326
|
+
keyring_usable = False
|
|
327
|
+
|
|
308
328
|
# Python info (highlight version and path in green)
|
|
309
329
|
env_table.add_row(
|
|
310
330
|
"Python Version", f"[green]{'.'.join(system_info['python_version'].split('.')[:3])}[/green]"
|
|
@@ -336,9 +356,15 @@ def show_check_summary() -> None:
|
|
|
336
356
|
)
|
|
337
357
|
else: # parsed successfully
|
|
338
358
|
env_table.add_row("Config File", f"[green]Found[/green] ({config_path})")
|
|
339
|
-
default_model_value = config_summary.get("default_model", "
|
|
359
|
+
default_model_value = config_summary.get("default_model", "gpt-5-mini.low (system default)")
|
|
340
360
|
env_table.add_row("Default Model", f"[green]{default_model_value}[/green]")
|
|
341
361
|
|
|
362
|
+
# Keyring backend (always shown in application-level settings)
|
|
363
|
+
if keyring_usable and keyring_name != "unavailable":
|
|
364
|
+
env_table.add_row("Keyring Backend", f"[green]{keyring_name}[/green]")
|
|
365
|
+
else:
|
|
366
|
+
env_table.add_row("Keyring Backend", "[red]not available[/red]")
|
|
367
|
+
|
|
342
368
|
console.print(env_table)
|
|
343
369
|
|
|
344
370
|
# Logger Settings panel with two-column layout
|
|
@@ -470,10 +496,15 @@ def show_check_summary() -> None:
|
|
|
470
496
|
if config_summary.get("status") == "parsed":
|
|
471
497
|
mcp_servers = config_summary.get("mcp_servers", [])
|
|
472
498
|
if mcp_servers:
|
|
499
|
+
from fast_agent.config import MCPServerSettings
|
|
500
|
+
from fast_agent.mcp.oauth_client import compute_server_identity
|
|
501
|
+
|
|
473
502
|
servers_table = Table(show_header=True, box=None)
|
|
474
503
|
servers_table.add_column("Name", style="white", header_style="bold bright_white")
|
|
475
504
|
servers_table.add_column("Transport", style="white", header_style="bold bright_white")
|
|
476
505
|
servers_table.add_column("Command/URL", header_style="bold bright_white")
|
|
506
|
+
servers_table.add_column("OAuth", header_style="bold bright_white")
|
|
507
|
+
servers_table.add_column("Token", header_style="bold bright_white")
|
|
477
508
|
|
|
478
509
|
for server in mcp_servers:
|
|
479
510
|
name = server["name"]
|
|
@@ -489,7 +520,48 @@ def show_check_summary() -> None:
|
|
|
489
520
|
if "Not configured" not in command_url:
|
|
490
521
|
command_url = f"[green]{command_url}[/green]"
|
|
491
522
|
|
|
492
|
-
|
|
523
|
+
# OAuth status and token presence
|
|
524
|
+
# Default for unsupported transports (e.g., STDIO): show "-" rather than "off"
|
|
525
|
+
oauth_status = "[dim]-[/dim]"
|
|
526
|
+
token_status = "[dim]n/a[/dim]"
|
|
527
|
+
# Attempt to reconstruct minimal server settings for identity check
|
|
528
|
+
try:
|
|
529
|
+
cfg = MCPServerSettings(
|
|
530
|
+
name=name,
|
|
531
|
+
transport="sse"
|
|
532
|
+
if transport == "SSE"
|
|
533
|
+
else ("stdio" if transport == "STDIO" else "http"),
|
|
534
|
+
url=(server.get("url") or None),
|
|
535
|
+
auth=server.get("auth") if isinstance(server.get("auth"), dict) else None,
|
|
536
|
+
)
|
|
537
|
+
except Exception:
|
|
538
|
+
cfg = None
|
|
539
|
+
|
|
540
|
+
if cfg and cfg.transport in ("http", "sse"):
|
|
541
|
+
# Determine if OAuth is enabled for this server
|
|
542
|
+
oauth_enabled = True
|
|
543
|
+
if cfg.auth is not None and hasattr(cfg.auth, "oauth"):
|
|
544
|
+
oauth_enabled = bool(getattr(cfg.auth, "oauth"))
|
|
545
|
+
oauth_status = "[green]on[/green]" if oauth_enabled else "[dim]off[/dim]"
|
|
546
|
+
|
|
547
|
+
# Only check token presence when using keyring persist
|
|
548
|
+
persist = "keyring"
|
|
549
|
+
if cfg.auth is not None and hasattr(cfg.auth, "persist"):
|
|
550
|
+
persist = getattr(cfg.auth, "persist") or "keyring"
|
|
551
|
+
if keyring and keyring_usable and persist == "keyring" and oauth_enabled:
|
|
552
|
+
identity = compute_server_identity(cfg)
|
|
553
|
+
tkey = f"oauth:tokens:{identity}"
|
|
554
|
+
try:
|
|
555
|
+
has = keyring.get_password("fast-agent-mcp", tkey) is not None
|
|
556
|
+
except Exception:
|
|
557
|
+
has = False
|
|
558
|
+
token_status = "[bold green]✓[/bold green]" if has else "[dim]✗[/dim]"
|
|
559
|
+
elif persist == "keyring" and not keyring_usable and oauth_enabled:
|
|
560
|
+
token_status = "[red]not available[/red]"
|
|
561
|
+
elif persist == "memory" and oauth_enabled:
|
|
562
|
+
token_status = "[yellow]memory[/yellow]"
|
|
563
|
+
|
|
564
|
+
servers_table.add_row(name, transport, command_url, oauth_status, token_status)
|
|
493
565
|
|
|
494
566
|
_print_section_header("MCP Servers", color="blue")
|
|
495
567
|
console.print(servers_table)
|
fast_agent/cli/commands/go.py
CHANGED
|
@@ -18,10 +18,16 @@ app = typer.Typer(
|
|
|
18
18
|
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
+
default_instruction = """You are a helpful AI Agent.
|
|
22
|
+
|
|
23
|
+
{{serverInstructions}}
|
|
24
|
+
|
|
25
|
+
The current date is {{currentDate}}."""
|
|
26
|
+
|
|
21
27
|
|
|
22
28
|
async def _run_agent(
|
|
23
29
|
name: str = "fast-agent cli",
|
|
24
|
-
instruction: str =
|
|
30
|
+
instruction: str = default_instruction,
|
|
25
31
|
config_path: Optional[str] = None,
|
|
26
32
|
server_list: Optional[List[str]] = None,
|
|
27
33
|
model: Optional[str] = None,
|
|
@@ -352,7 +358,7 @@ def go(
|
|
|
352
358
|
stdio_commands.append(stdio)
|
|
353
359
|
|
|
354
360
|
# Resolve instruction from file/URL or use default
|
|
355
|
-
resolved_instruction =
|
|
361
|
+
resolved_instruction = default_instruction # Default
|
|
356
362
|
agent_name = "agent"
|
|
357
363
|
|
|
358
364
|
if instruction:
|
|
@@ -8,11 +8,13 @@ from rich.console import Console
|
|
|
8
8
|
from rich.panel import Panel
|
|
9
9
|
from rich.table import Table
|
|
10
10
|
|
|
11
|
+
from fast_agent.ui.console import console as shared_console
|
|
12
|
+
|
|
11
13
|
app = typer.Typer(
|
|
12
14
|
help="Create fast-agent quickstarts",
|
|
13
15
|
no_args_is_help=False, # Allow showing our custom help instead
|
|
14
16
|
)
|
|
15
|
-
console =
|
|
17
|
+
console = shared_console
|
|
16
18
|
|
|
17
19
|
EXAMPLE_TYPES = {
|
|
18
20
|
"workflow": {
|
|
@@ -89,7 +89,7 @@ async def add_servers_to_config(fast_app: Any, servers: Dict[str, Dict[str, Any]
|
|
|
89
89
|
):
|
|
90
90
|
fast_app.app.context.config.mcp.servers = {}
|
|
91
91
|
|
|
92
|
-
# Add each server to the config
|
|
92
|
+
# Add each server to the config (and keep the runtime registry in sync)
|
|
93
93
|
for server_name, server_config in servers.items():
|
|
94
94
|
# Build server settings based on transport type
|
|
95
95
|
server_settings = {"transport": server_config["transport"]}
|
|
@@ -103,4 +103,12 @@ async def add_servers_to_config(fast_app: Any, servers: Dict[str, Dict[str, Any]
|
|
|
103
103
|
if "headers" in server_config:
|
|
104
104
|
server_settings["headers"] = server_config["headers"]
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
mcp_server = MCPServerSettings(**server_settings)
|
|
107
|
+
# Update config model
|
|
108
|
+
fast_app.app.context.config.mcp.servers[server_name] = mcp_server
|
|
109
|
+
# Ensure ServerRegistry sees dynamic additions even when no config file exists
|
|
110
|
+
if (
|
|
111
|
+
hasattr(fast_app.app.context, "server_registry")
|
|
112
|
+
and fast_app.app.context.server_registry is not None
|
|
113
|
+
):
|
|
114
|
+
fast_app.app.context.server_registry.registry[server_name] = mcp_server
|
fast_agent/cli/commands/setup.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
3
|
import typer
|
|
4
|
-
from rich.console import Console
|
|
5
4
|
from rich.prompt import Confirm
|
|
6
5
|
|
|
6
|
+
from fast_agent.ui.console import console as shared_console
|
|
7
|
+
|
|
7
8
|
app = typer.Typer()
|
|
8
|
-
console =
|
|
9
|
+
console = shared_console
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def load_template_text(filename: str) -> str:
|
|
@@ -25,9 +26,7 @@ def load_template_text(filename: str) -> str:
|
|
|
25
26
|
res_name = "pyproject.toml.tmpl"
|
|
26
27
|
else:
|
|
27
28
|
res_name = filename
|
|
28
|
-
resource_path = (
|
|
29
|
-
files("fast_agent").joinpath("resources").joinpath("setup").joinpath(res_name)
|
|
30
|
-
)
|
|
29
|
+
resource_path = files("fast_agent").joinpath("resources").joinpath("setup").joinpath(res_name)
|
|
31
30
|
if resource_path.is_file():
|
|
32
31
|
return resource_path.read_text()
|
|
33
32
|
|
|
@@ -136,9 +135,8 @@ def init(
|
|
|
136
135
|
# Always use latest fast-agent-mcp (no version pin)
|
|
137
136
|
fast_agent_dep = '"fast-agent-mcp"'
|
|
138
137
|
|
|
139
|
-
return (
|
|
140
|
-
|
|
141
|
-
.replace("{{fast_agent_dep}}", fast_agent_dep)
|
|
138
|
+
return template_text.replace("{{python_requires}}", py_req).replace(
|
|
139
|
+
"{{fast_agent_dep}}", fast_agent_dep
|
|
142
140
|
)
|
|
143
141
|
|
|
144
142
|
pyproject_template = load_template_text("pyproject.toml")
|
|
@@ -168,7 +166,7 @@ def init(
|
|
|
168
166
|
"2. Keep fastagent.secrets.yaml secure and never commit it to version control"
|
|
169
167
|
)
|
|
170
168
|
console.print(
|
|
171
|
-
"3. Update fastagent.config.yaml to set a default model (currently system default is '
|
|
169
|
+
"3. Update fastagent.config.yaml to set a default model (currently system default is 'gpt-5-mini.low')"
|
|
172
170
|
)
|
|
173
171
|
console.print("\nTo get started, run:")
|
|
174
172
|
console.print(" uv run agent.py")
|
fast_agent/cli/constants.py
CHANGED
|
@@ -22,4 +22,4 @@ GO_SPECIFIC_OPTIONS = {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
# Known subcommands that should not trigger auto-routing
|
|
25
|
-
KNOWN_SUBCOMMANDS = {"go", "setup", "check", "bootstrap", "quickstart", "--help", "-h", "--version"}
|
|
25
|
+
KNOWN_SUBCOMMANDS = {"go", "setup", "check", "auth", "bootstrap", "quickstart", "--help", "-h", "--version"}
|
fast_agent/cli/main.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import typer
|
|
4
4
|
from rich.table import Table
|
|
5
5
|
|
|
6
|
-
from fast_agent.cli.commands import check_config, go, quickstart, setup
|
|
6
|
+
from fast_agent.cli.commands import auth, check_config, go, quickstart, setup
|
|
7
7
|
from fast_agent.cli.terminal import Application
|
|
8
8
|
from fast_agent.ui.console import console as shared_console
|
|
9
9
|
|
|
@@ -16,6 +16,7 @@ app = typer.Typer(
|
|
|
16
16
|
app.add_typer(go.app, name="go", help="Run an interactive agent directly from the command line")
|
|
17
17
|
app.add_typer(setup.app, name="setup", help="Set up a new agent project")
|
|
18
18
|
app.add_typer(check_config.app, name="check", help="Show or diagnose fast-agent configuration")
|
|
19
|
+
app.add_typer(auth.app, name="auth", help="Manage OAuth authentication for MCP servers")
|
|
19
20
|
app.add_typer(quickstart.app, name="bootstrap", help="Create example applications")
|
|
20
21
|
app.add_typer(quickstart.app, name="quickstart", help="Create example applications")
|
|
21
22
|
|
|
@@ -62,6 +63,7 @@ def show_welcome() -> None:
|
|
|
62
63
|
|
|
63
64
|
table.add_row("[bold]go[/bold]", "Start an interactive session")
|
|
64
65
|
table.add_row("check", "Show current configuration")
|
|
66
|
+
table.add_row("auth", "Manage OAuth tokens and keyring")
|
|
65
67
|
table.add_row("setup", "Create agent template and configuration")
|
|
66
68
|
table.add_row("quickstart", "Create example applications (workflow, researcher, etc.)")
|
|
67
69
|
|
fast_agent/config.py
CHANGED
|
@@ -9,20 +9,35 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Any, Dict, List, Literal, Optional, Tuple
|
|
10
10
|
|
|
11
11
|
from mcp import Implementation
|
|
12
|
-
from pydantic import BaseModel, ConfigDict, field_validator
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
|
13
13
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class MCPServerAuthSettings(BaseModel):
|
|
17
|
-
"""Represents authentication configuration for a server.
|
|
17
|
+
"""Represents authentication configuration for a server.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Minimal OAuth v2.1 support with sensible defaults.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Enable OAuth for SSE/HTTP transports. If None is provided for the auth block,
|
|
23
|
+
# the system will assume OAuth is enabled by default.
|
|
24
|
+
oauth: bool = True
|
|
25
|
+
|
|
26
|
+
# Local callback server configuration
|
|
27
|
+
redirect_port: int = 3030
|
|
28
|
+
redirect_path: str = "/callback"
|
|
29
|
+
|
|
30
|
+
# Optional scope override. If set to a list, values are space-joined.
|
|
31
|
+
scope: str | list[str] | None = None
|
|
32
|
+
|
|
33
|
+
# Token persistence: use OS keychain via 'keyring' by default; fallback to 'memory'.
|
|
34
|
+
persist: Literal["keyring", "memory"] = "keyring"
|
|
20
35
|
|
|
21
36
|
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
|
|
22
37
|
|
|
23
38
|
|
|
24
39
|
class MCPSamplingSettings(BaseModel):
|
|
25
|
-
model: str = "
|
|
40
|
+
model: str = "gpt-5-mini.low"
|
|
26
41
|
|
|
27
42
|
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
|
|
28
43
|
|
|
@@ -107,8 +122,47 @@ class MCPServerSettings(BaseModel):
|
|
|
107
122
|
cwd: str | None = None
|
|
108
123
|
"""Working directory for the executed server command."""
|
|
109
124
|
|
|
125
|
+
include_instructions: bool = True
|
|
126
|
+
"""Whether to include this server's instructions in the system prompt (default: True)."""
|
|
127
|
+
|
|
110
128
|
implementation: Implementation | None = None
|
|
111
129
|
|
|
130
|
+
@model_validator(mode="before")
|
|
131
|
+
@classmethod
|
|
132
|
+
def validate_transport_inference(cls, values):
|
|
133
|
+
"""Automatically infer transport type based on url/command presence."""
|
|
134
|
+
import warnings
|
|
135
|
+
|
|
136
|
+
if isinstance(values, dict):
|
|
137
|
+
# Check if transport was explicitly provided in the input
|
|
138
|
+
transport_explicit = "transport" in values
|
|
139
|
+
url = values.get("url")
|
|
140
|
+
command = values.get("command")
|
|
141
|
+
|
|
142
|
+
# Only infer if transport was not explicitly set
|
|
143
|
+
if not transport_explicit:
|
|
144
|
+
# Check if we have both url and command specified
|
|
145
|
+
has_url = url is not None and str(url).strip()
|
|
146
|
+
has_command = command is not None and str(command).strip()
|
|
147
|
+
|
|
148
|
+
if has_url and has_command:
|
|
149
|
+
warnings.warn(
|
|
150
|
+
f"MCP Server config has both 'url' ({url}) and 'command' ({command}) specified. "
|
|
151
|
+
"Preferring HTTP transport and ignoring command.",
|
|
152
|
+
UserWarning,
|
|
153
|
+
stacklevel=4,
|
|
154
|
+
)
|
|
155
|
+
values["transport"] = "http"
|
|
156
|
+
values["command"] = None # Clear command to avoid confusion
|
|
157
|
+
elif has_url and not has_command:
|
|
158
|
+
values["transport"] = "http"
|
|
159
|
+
elif has_command and not has_url:
|
|
160
|
+
# Keep default "stdio" for command-based servers
|
|
161
|
+
values["transport"] = "stdio"
|
|
162
|
+
# If neither url nor command is specified, keep default "stdio"
|
|
163
|
+
|
|
164
|
+
return values
|
|
165
|
+
|
|
112
166
|
|
|
113
167
|
class MCPSettings(BaseModel):
|
|
114
168
|
"""Configuration for all MCP servers."""
|
|
@@ -260,8 +314,8 @@ class TensorZeroSettings(BaseModel):
|
|
|
260
314
|
Settings for using TensorZero via its OpenAI-compatible API.
|
|
261
315
|
"""
|
|
262
316
|
|
|
263
|
-
base_url:
|
|
264
|
-
api_key:
|
|
317
|
+
base_url: str | None = None
|
|
318
|
+
api_key: str | None = None
|
|
265
319
|
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
|
|
266
320
|
|
|
267
321
|
|
|
@@ -287,7 +341,7 @@ class HuggingFaceSettings(BaseModel):
|
|
|
287
341
|
Settings for HuggingFace authentication (used for MCP connections).
|
|
288
342
|
"""
|
|
289
343
|
|
|
290
|
-
api_key:
|
|
344
|
+
api_key: str | None = None
|
|
291
345
|
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
|
|
292
346
|
|
|
293
347
|
|
|
@@ -408,7 +462,7 @@ class Settings(BaseSettings):
|
|
|
408
462
|
execution_engine: Literal["asyncio"] = "asyncio"
|
|
409
463
|
"""Execution engine for the fast-agent application"""
|
|
410
464
|
|
|
411
|
-
default_model: str | None = "
|
|
465
|
+
default_model: str | None = "gpt-5-mini.low"
|
|
412
466
|
"""
|
|
413
467
|
Default model for agents. Format is provider.model_name.<reasoning_effort>, for example openai.o3-mini.low
|
|
414
468
|
Aliases are provided for common models e.g. sonnet, haiku, gpt-4.1, o3-mini etc.
|
|
@@ -459,7 +513,7 @@ class Settings(BaseSettings):
|
|
|
459
513
|
groq: GroqSettings | None = None
|
|
460
514
|
"""Settings for using the Groq provider in the fast-agent application"""
|
|
461
515
|
|
|
462
|
-
logger: LoggerSettings
|
|
516
|
+
logger: LoggerSettings = LoggerSettings()
|
|
463
517
|
"""Logger settings for the fast-agent application"""
|
|
464
518
|
|
|
465
519
|
# MCP UI integration mode for handling ui:// embedded resources from MCP tool results
|
fast_agent/mcp/mcp_aggregator.py
CHANGED
|
@@ -461,6 +461,36 @@ class MCPAggregator(ContextDependent):
|
|
|
461
461
|
for server_name in self.server_names:
|
|
462
462
|
await self._refresh_server_tools(server_name)
|
|
463
463
|
|
|
464
|
+
async def get_server_instructions(self) -> Dict[str, tuple[str, List[str]]]:
|
|
465
|
+
"""
|
|
466
|
+
Get instructions from all connected servers along with their tool names.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
Dict mapping server name to tuple of (instructions, list of tool names)
|
|
470
|
+
"""
|
|
471
|
+
instructions = {}
|
|
472
|
+
|
|
473
|
+
if self.connection_persistence and hasattr(self, '_persistent_connection_manager'):
|
|
474
|
+
# Get instructions from persistent connections
|
|
475
|
+
for server_name in self.server_names:
|
|
476
|
+
try:
|
|
477
|
+
server_conn = await self._persistent_connection_manager.get_server(
|
|
478
|
+
server_name, client_session_factory=self._create_session_factory(server_name)
|
|
479
|
+
)
|
|
480
|
+
# Always include server, even if no instructions
|
|
481
|
+
# Get tool names for this server
|
|
482
|
+
tool_names = [
|
|
483
|
+
namespaced_tool.tool.name
|
|
484
|
+
for namespaced_tool_name, namespaced_tool in self._namespaced_tool_map.items()
|
|
485
|
+
if namespaced_tool.server_name == server_name
|
|
486
|
+
]
|
|
487
|
+
# Include server even if instructions is None
|
|
488
|
+
instructions[server_name] = (server_conn.server_instructions, tool_names)
|
|
489
|
+
except Exception as e:
|
|
490
|
+
logger.debug(f"Failed to get instructions from server {server_name}: {e}")
|
|
491
|
+
|
|
492
|
+
return instructions
|
|
493
|
+
|
|
464
494
|
async def _execute_on_server(
|
|
465
495
|
self,
|
|
466
496
|
server_name: str,
|
|
@@ -33,6 +33,7 @@ from fast_agent.core.logging.logger import get_logger
|
|
|
33
33
|
from fast_agent.event_progress import ProgressAction
|
|
34
34
|
from fast_agent.mcp.logger_textio import get_stderr_handler
|
|
35
35
|
from fast_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
|
|
36
|
+
from fast_agent.mcp.oauth_client import build_oauth_provider
|
|
36
37
|
|
|
37
38
|
if TYPE_CHECKING:
|
|
38
39
|
from fast_agent.context import Context
|
|
@@ -104,6 +105,9 @@ class ServerConnection:
|
|
|
104
105
|
self._error_occurred = False
|
|
105
106
|
self._error_message = None
|
|
106
107
|
|
|
108
|
+
# Server instructions from initialization
|
|
109
|
+
self.server_instructions: str | None = None
|
|
110
|
+
|
|
107
111
|
def is_healthy(self) -> bool:
|
|
108
112
|
"""Check if the server connection is healthy and ready to use."""
|
|
109
113
|
return self.session is not None and not self._error_occurred
|
|
@@ -130,10 +134,20 @@ class ServerConnection:
|
|
|
130
134
|
Initializes the server connection and session.
|
|
131
135
|
Must be called within an async context.
|
|
132
136
|
"""
|
|
133
|
-
|
|
137
|
+
assert self.session, "Session must be created before initialization"
|
|
134
138
|
result = await self.session.initialize()
|
|
135
139
|
|
|
136
140
|
self.server_capabilities = result.capabilities
|
|
141
|
+
|
|
142
|
+
# Store instructions if provided by the server and enabled in config
|
|
143
|
+
if self.server_config.include_instructions:
|
|
144
|
+
self.server_instructions = getattr(result, 'instructions', None)
|
|
145
|
+
if self.server_instructions:
|
|
146
|
+
logger.debug(f"{self.server_name}: Received server instructions", data={"instructions": self.server_instructions})
|
|
147
|
+
else:
|
|
148
|
+
self.server_instructions = None
|
|
149
|
+
logger.debug(f"{self.server_name}: Server instructions disabled by configuration")
|
|
150
|
+
|
|
137
151
|
# If there's an init hook, run it
|
|
138
152
|
|
|
139
153
|
# Now the session is ready for use
|
|
@@ -341,6 +355,10 @@ class MCPConnectionManager(ContextDependent):
|
|
|
341
355
|
|
|
342
356
|
def transport_context_factory():
|
|
343
357
|
if config.transport == "stdio":
|
|
358
|
+
if not config.command:
|
|
359
|
+
raise ValueError(
|
|
360
|
+
f"Server '{server_name}' uses stdio transport but no command is specified"
|
|
361
|
+
)
|
|
344
362
|
server_params = StdioServerParameters(
|
|
345
363
|
command=config.command,
|
|
346
364
|
args=config.args if config.args is not None else [],
|
|
@@ -353,18 +371,37 @@ class MCPConnectionManager(ContextDependent):
|
|
|
353
371
|
logger.debug(f"{server_name}: Creating stdio client with custom error handler")
|
|
354
372
|
return _add_none_to_context(stdio_client(server_params, errlog=error_handler))
|
|
355
373
|
elif config.transport == "sse":
|
|
374
|
+
if not config.url:
|
|
375
|
+
raise ValueError(
|
|
376
|
+
f"Server '{server_name}' uses sse transport but no url is specified"
|
|
377
|
+
)
|
|
356
378
|
# Suppress MCP library error spam
|
|
357
379
|
self._suppress_mcp_sse_errors()
|
|
358
|
-
|
|
380
|
+
oauth_auth = build_oauth_provider(config)
|
|
381
|
+
# If using OAuth, strip any pre-existing Authorization headers to avoid conflicts
|
|
382
|
+
headers = dict(config.headers or {})
|
|
383
|
+
if oauth_auth is not None:
|
|
384
|
+
headers.pop("Authorization", None)
|
|
385
|
+
headers.pop("X-HF-Authorization", None)
|
|
359
386
|
return _add_none_to_context(
|
|
360
387
|
sse_client(
|
|
361
388
|
config.url,
|
|
362
|
-
|
|
389
|
+
headers,
|
|
363
390
|
sse_read_timeout=config.read_transport_sse_timeout_seconds,
|
|
391
|
+
auth=oauth_auth,
|
|
364
392
|
)
|
|
365
393
|
)
|
|
366
394
|
elif config.transport == "http":
|
|
367
|
-
|
|
395
|
+
if not config.url:
|
|
396
|
+
raise ValueError(
|
|
397
|
+
f"Server '{server_name}' uses http transport but no url is specified"
|
|
398
|
+
)
|
|
399
|
+
oauth_auth = build_oauth_provider(config)
|
|
400
|
+
headers = dict(config.headers or {})
|
|
401
|
+
if oauth_auth is not None:
|
|
402
|
+
headers.pop("Authorization", None)
|
|
403
|
+
headers.pop("X-HF-Authorization", None)
|
|
404
|
+
return streamablehttp_client(config.url, headers, auth=oauth_auth)
|
|
368
405
|
else:
|
|
369
406
|
raise ValueError(f"Unsupported transport: {config.transport}")
|
|
370
407
|
|