fast-agent-mcp 0.3.3__py3-none-any.whl → 0.3.5__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 (44) hide show
  1. fast_agent/__init__.py +6 -3
  2. fast_agent/agents/__init__.py +63 -14
  3. fast_agent/cli/__main__.py +8 -5
  4. fast_agent/cli/commands/auth.py +370 -0
  5. fast_agent/cli/commands/check_config.py +54 -3
  6. fast_agent/cli/commands/go.py +1 -1
  7. fast_agent/cli/commands/quickstart.py +3 -1
  8. fast_agent/cli/commands/server_helpers.py +10 -2
  9. fast_agent/cli/commands/setup.py +3 -2
  10. fast_agent/cli/constants.py +1 -1
  11. fast_agent/cli/main.py +3 -1
  12. fast_agent/config.py +63 -8
  13. fast_agent/core/__init__.py +38 -37
  14. fast_agent/core/direct_factory.py +1 -1
  15. fast_agent/mcp/mcp_connection_manager.py +21 -3
  16. fast_agent/mcp/oauth_client.py +481 -0
  17. fast_agent/mcp/ui_agent.py +1 -1
  18. fast_agent/resources/examples/data-analysis/analysis-campaign.py +1 -1
  19. fast_agent/resources/examples/data-analysis/analysis.py +1 -1
  20. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +1 -1
  21. fast_agent/resources/examples/mcp/elicitations/game_character.py +1 -1
  22. fast_agent/resources/examples/mcp/elicitations/tool_call.py +1 -1
  23. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +1 -1
  24. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +1 -1
  25. fast_agent/resources/examples/researcher/researcher-eval.py +1 -1
  26. fast_agent/resources/examples/researcher/researcher-imp.py +1 -1
  27. fast_agent/resources/examples/researcher/researcher.py +1 -1
  28. fast_agent/resources/examples/tensorzero/agent.py +1 -1
  29. fast_agent/resources/examples/tensorzero/image_demo.py +1 -1
  30. fast_agent/resources/examples/tensorzero/simple_agent.py +1 -1
  31. fast_agent/resources/examples/workflows/chaining.py +1 -1
  32. fast_agent/resources/examples/workflows/evaluator.py +1 -1
  33. fast_agent/resources/examples/workflows/human_input.py +1 -1
  34. fast_agent/resources/examples/workflows/orchestrator.py +1 -1
  35. fast_agent/resources/examples/workflows/parallel.py +1 -1
  36. fast_agent/resources/examples/workflows/router.py +1 -1
  37. fast_agent/resources/setup/agent.py +1 -1
  38. fast_agent/resources/setup/fastagent.config.yaml +2 -2
  39. fast_agent/ui/mcp_ui_utils.py +12 -1
  40. {fast_agent_mcp-0.3.3.dist-info → fast_agent_mcp-0.3.5.dist-info}/METADATA +40 -3
  41. {fast_agent_mcp-0.3.3.dist-info → fast_agent_mcp-0.3.5.dist-info}/RECORD +44 -42
  42. {fast_agent_mcp-0.3.3.dist-info → fast_agent_mcp-0.3.5.dist-info}/WHEEL +0 -0
  43. {fast_agent_mcp-0.3.3.dist-info → fast_agent_mcp-0.3.5.dist-info}/entry_points.txt +0 -0
  44. {fast_agent_mcp-0.3.3.dist-info → fast_agent_mcp-0.3.5.dist-info}/licenses/LICENSE +0 -0
fast_agent/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """fast-agent - An MCP native agent application framework"""
2
- from typing import TYPE_CHECKING as _TYPE_CHECKING
2
+
3
+ from typing import TYPE_CHECKING
3
4
 
4
5
  # Configuration and settings (safe - pure Pydantic models)
5
6
  from fast_agent.config import (
@@ -73,10 +74,12 @@ def __getattr__(name: str):
73
74
 
74
75
  return ToolAgent
75
76
  elif name == "McpAgent":
77
+ # Import directly from submodule to avoid package re-import cycles
76
78
  from fast_agent.agents.mcp_agent import McpAgent
77
79
 
78
80
  return McpAgent
79
81
  elif name == "FastAgent":
82
+ # Import from the canonical implementation to avoid recursive imports
80
83
  from fast_agent.core.fastagent import FastAgent
81
84
 
82
85
  return FastAgent
@@ -85,7 +88,8 @@ def __getattr__(name: str):
85
88
 
86
89
 
87
90
  # Help static analyzers/IDEs resolve symbols and signatures without importing at runtime.
88
- if _TYPE_CHECKING: # pragma: no cover - typing aid only
91
+ if TYPE_CHECKING: # pragma: no cover - typing aid only
92
+ # Provide a concrete import path for type checkers/IDEs
89
93
  from fast_agent.core.fastagent import FastAgent as FastAgent # noqa: F401
90
94
 
91
95
 
@@ -124,7 +128,6 @@ __all__ = [
124
128
  "LlmStopReason",
125
129
  "RequestParams",
126
130
  # Agents (lazy loaded)
127
- "ToolAgentSynchronous",
128
131
  "LlmAgent",
129
132
  "LlmDecorator",
130
133
  "ToolAgent",
@@ -1,23 +1,72 @@
1
1
  """
2
2
  Fast Agent - Agent implementations and workflow patterns.
3
3
 
4
- This module exports all agent classes from the fast_agent.agents package,
5
- providing a single import point for both core agents and workflow agents.
4
+ This module re-exports agent classes with lazy imports to avoid circular
5
+ dependencies during package initialization while preserving a clean API:
6
+
7
+ from fast_agent.agents import McpAgent, ToolAgent, LlmAgent
6
8
  """
7
9
 
8
- # Core agents
10
+ from typing import TYPE_CHECKING
11
+
9
12
  from fast_agent.agents.agent_types import AgentConfig
10
- from fast_agent.agents.llm_agent import LlmAgent
11
- from fast_agent.agents.llm_decorator import LlmDecorator
12
- from fast_agent.agents.mcp_agent import McpAgent
13
- from fast_agent.agents.tool_agent import ToolAgent
14
-
15
- # Workflow agents
16
- from fast_agent.agents.workflow.chain_agent import ChainAgent
17
- from fast_agent.agents.workflow.evaluator_optimizer import EvaluatorOptimizerAgent
18
- from fast_agent.agents.workflow.iterative_planner import IterativePlanner
19
- from fast_agent.agents.workflow.parallel_agent import ParallelAgent
20
- from fast_agent.agents.workflow.router_agent import RouterAgent
13
+
14
+
15
+ def __getattr__(name: str):
16
+ """Lazily resolve agent classes to avoid import cycles."""
17
+ if name == "LlmAgent":
18
+ from .llm_agent import LlmAgent
19
+
20
+ return LlmAgent
21
+ elif name == "LlmDecorator":
22
+ from .llm_decorator import LlmDecorator
23
+
24
+ return LlmDecorator
25
+ elif name == "ToolAgent":
26
+ from .tool_agent import ToolAgent
27
+
28
+ return ToolAgent
29
+ elif name == "McpAgent":
30
+ from .mcp_agent import McpAgent
31
+
32
+ return McpAgent
33
+ elif name == "ChainAgent":
34
+ from .workflow.chain_agent import ChainAgent
35
+
36
+ return ChainAgent
37
+ elif name == "EvaluatorOptimizerAgent":
38
+ from .workflow.evaluator_optimizer import EvaluatorOptimizerAgent
39
+
40
+ return EvaluatorOptimizerAgent
41
+ elif name == "IterativePlanner":
42
+ from .workflow.iterative_planner import IterativePlanner
43
+
44
+ return IterativePlanner
45
+ elif name == "ParallelAgent":
46
+ from .workflow.parallel_agent import ParallelAgent
47
+
48
+ return ParallelAgent
49
+ elif name == "RouterAgent":
50
+ from .workflow.router_agent import RouterAgent
51
+
52
+ return RouterAgent
53
+ else:
54
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
55
+
56
+
57
+ if TYPE_CHECKING: # pragma: no cover - type checking only
58
+ from .llm_agent import LlmAgent as LlmAgent # noqa: F401
59
+ from .llm_decorator import LlmDecorator as LlmDecorator # noqa: F401
60
+ from .mcp_agent import McpAgent as McpAgent # noqa: F401
61
+ from .tool_agent import ToolAgent as ToolAgent # noqa: F401
62
+ from .workflow.chain_agent import ChainAgent as ChainAgent # noqa: F401
63
+ from .workflow.evaluator_optimizer import (
64
+ EvaluatorOptimizerAgent as EvaluatorOptimizerAgent,
65
+ ) # noqa: F401
66
+ from .workflow.iterative_planner import IterativePlanner as IterativePlanner # noqa: F401
67
+ from .workflow.parallel_agent import ParallelAgent as ParallelAgent # noqa: F401
68
+ from .workflow.router_agent import RouterAgent as RouterAgent # noqa: F401
69
+
21
70
 
22
71
  __all__ = [
23
72
  # Core agents
@@ -13,14 +13,17 @@ def main():
13
13
  # Check if first arg is not already a subcommand
14
14
  first_arg = sys.argv[1]
15
15
 
16
- if first_arg not in KNOWN_SUBCOMMANDS and any(
17
- arg in sys.argv or any(arg.startswith(opt + "=") for opt in GO_SPECIFIC_OPTIONS)
18
- for arg in sys.argv
19
- ):
16
+ # Only auto-route if any known go-specific options are present
17
+ has_go_options = any(
18
+ (arg in GO_SPECIFIC_OPTIONS) or any(arg.startswith(opt + "=") for opt in GO_SPECIFIC_OPTIONS)
19
+ for arg in sys.argv[1:]
20
+ )
21
+
22
+ if first_arg not in KNOWN_SUBCOMMANDS and has_go_options:
20
23
  # Find where to insert 'go' - before the first go-specific option
21
24
  insert_pos = 1
22
25
  for i, arg in enumerate(sys.argv[1:], 1):
23
- if arg in GO_SPECIFIC_OPTIONS or any(
26
+ if (arg in GO_SPECIFIC_OPTIONS) or any(
24
27
  arg.startswith(opt + "=") for opt in GO_SPECIFIC_OPTIONS
25
28
  ):
26
29
  insert_pos = i
@@ -0,0 +1,370 @@
1
+ """Authentication management commands for fast-agent.
2
+
3
+ Shows keyring backend, per-server OAuth token status, and provides a way to clear tokens.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Dict, List, Optional
9
+
10
+ import typer
11
+ from rich.table import Table
12
+
13
+ from fast_agent.config import Settings, get_settings
14
+ from fast_agent.mcp.oauth_client import (
15
+ _derive_base_server_url,
16
+ clear_keyring_token,
17
+ compute_server_identity,
18
+ list_keyring_tokens,
19
+ )
20
+ from fast_agent.ui.console import console
21
+
22
+ app = typer.Typer(help="Manage OAuth authentication state for MCP servers")
23
+
24
+
25
+ def _get_keyring_backend_name() -> str:
26
+ try:
27
+ import keyring
28
+
29
+ kr = keyring.get_keyring()
30
+ return getattr(kr, "name", kr.__class__.__name__)
31
+ except Exception:
32
+ return "unavailable"
33
+
34
+
35
+ def _keyring_get_password(service: str, username: str) -> str | None:
36
+ try:
37
+ import keyring
38
+
39
+ return keyring.get_password(service, username)
40
+ except Exception:
41
+ return None
42
+
43
+
44
+ def _keyring_delete_password(service: str, username: str) -> bool:
45
+ try:
46
+ import keyring
47
+
48
+ keyring.delete_password(service, username)
49
+ return True
50
+ except Exception:
51
+ return False
52
+
53
+
54
+ def _server_rows_from_settings(settings: Settings):
55
+ rows = []
56
+ mcp = getattr(settings, "mcp", None)
57
+ servers = getattr(mcp, "servers", {}) if mcp else {}
58
+ for name, cfg in servers.items():
59
+ transport = getattr(cfg, "transport", "")
60
+ if transport == "stdio":
61
+ # STDIO servers do not use OAuth; skip in auth views
62
+ continue
63
+ url = getattr(cfg, "url", None)
64
+ auth = getattr(cfg, "auth", None)
65
+ oauth_enabled = getattr(auth, "oauth", True) if auth is not None else True
66
+ persist = getattr(auth, "persist", "keyring") if auth is not None else "keyring"
67
+ identity = compute_server_identity(cfg)
68
+ # token presence only meaningful if persist is keyring and transport is http/sse
69
+ has_token = False
70
+ if persist == "keyring" and transport in ("http", "sse") and oauth_enabled:
71
+ has_token = (
72
+ _keyring_get_password("fast-agent-mcp", f"oauth:tokens:{identity}") is not None
73
+ )
74
+ rows.append(
75
+ {
76
+ "name": name,
77
+ "transport": transport,
78
+ "url": url or "",
79
+ "persist": persist,
80
+ "oauth": oauth_enabled and transport in ("http", "sse"),
81
+ "has_token": has_token,
82
+ "identity": identity,
83
+ }
84
+ )
85
+ return rows
86
+
87
+
88
+ def _servers_by_identity(settings: Settings) -> Dict[str, List[str]]:
89
+ """Group configured server names by derived identity (base URL)."""
90
+ mapping: Dict[str, List[str]] = {}
91
+ mcp = getattr(settings, "mcp", None)
92
+ servers = getattr(mcp, "servers", {}) if mcp else {}
93
+ for name, cfg in servers.items():
94
+ try:
95
+ identity = compute_server_identity(cfg)
96
+ except Exception:
97
+ identity = name
98
+ mapping.setdefault(identity, []).append(name)
99
+ return mapping
100
+
101
+
102
+ @app.command()
103
+ def status(
104
+ target: Optional[str] = typer.Argument(None, help="Identity (base URL) or server name"),
105
+ config_path: Optional[str] = typer.Option(None, "--config-path", "-c"),
106
+ ) -> None:
107
+ """Show keyring backend and token status for configured MCP servers."""
108
+ settings = get_settings(config_path)
109
+ backend = _get_keyring_backend_name()
110
+
111
+ # Single-target view if target provided
112
+ if target:
113
+ settings = get_settings(config_path)
114
+ identity = _derive_base_server_url(target) if "://" in target else None
115
+ if not identity:
116
+ servers = getattr(getattr(settings, "mcp", None), "servers", {}) or {}
117
+ cfg = servers.get(target)
118
+ if not cfg:
119
+ typer.echo(f"Server '{target}' not found in config; treating as identity")
120
+ identity = target
121
+ else:
122
+ identity = compute_server_identity(cfg)
123
+
124
+ # Direct presence check
125
+ present = False
126
+ try:
127
+ import keyring
128
+
129
+ present = keyring.get_password("fast-agent-mcp", f"oauth:tokens:{identity}") is not None
130
+ except Exception:
131
+ present = False
132
+
133
+ table = Table(show_header=True, box=None)
134
+ table.add_column("Identity", header_style="bold")
135
+ table.add_column("Token", header_style="bold")
136
+ table.add_column("Servers", header_style="bold")
137
+ by_id = _servers_by_identity(settings)
138
+ servers_for_id = ", ".join(by_id.get(identity, [])) or "[dim]None[/dim]"
139
+ token_disp = "[bold green]✓[/bold green]" if present else "[dim]✗[/dim]"
140
+ table.add_row(identity, token_disp, servers_for_id)
141
+
142
+ console.print(f"Keyring backend: [green]{backend}[/green]")
143
+ console.print(table)
144
+ console.print(
145
+ "\n[dim]Run 'fast-agent auth clear --identity "
146
+ f"{identity}[/dim][dim]' to remove this token, or 'fast-agent auth clear --all' to remove all.[/dim]"
147
+ )
148
+ return
149
+
150
+ # Full status view
151
+ console.print(f"Keyring backend: [green]{backend}[/green]")
152
+
153
+ tokens = list_keyring_tokens()
154
+ token_table = Table(show_header=True, box=None)
155
+ token_table.add_column("Stored Tokens (Identity)", header_style="bold")
156
+ token_table.add_column("Present", header_style="bold")
157
+ if tokens:
158
+ for ident in tokens:
159
+ token_table.add_row(ident, "[bold green]✓[/bold green]")
160
+ else:
161
+ token_table.add_row("[dim]None[/dim]", "[dim]✗[/dim]")
162
+
163
+ console.print(token_table)
164
+
165
+ rows = _server_rows_from_settings(settings)
166
+ if rows:
167
+ map_table = Table(show_header=True, box=None)
168
+ map_table.add_column("Server", header_style="bold")
169
+ map_table.add_column("Transport", header_style="bold")
170
+ map_table.add_column("OAuth", header_style="bold")
171
+ map_table.add_column("Persist", header_style="bold")
172
+ map_table.add_column("Token", header_style="bold")
173
+ map_table.add_column("Identity", header_style="bold")
174
+ for row in rows:
175
+ oauth_status = "[green]on[/green]" if row["oauth"] else "[dim]off[/dim]"
176
+ persist = row["persist"]
177
+ persist_disp = (
178
+ f"[green]{persist}[/green]"
179
+ if persist == "keyring"
180
+ else f"[yellow]{persist}[/yellow]"
181
+ )
182
+ # Direct presence check for each identity so status works even without index
183
+ has_token = False
184
+ if persist == "keyring" and row["oauth"]:
185
+ try:
186
+ import keyring
187
+
188
+ has_token = (
189
+ keyring.get_password("fast-agent-mcp", f"oauth:tokens:{row['identity']}")
190
+ is not None
191
+ )
192
+ except Exception:
193
+ has_token = False
194
+ token_disp = (
195
+ "[bold green]✓[/bold green]"
196
+ if has_token
197
+ else (
198
+ "[yellow]memory[/yellow]"
199
+ if persist == "memory" and row["oauth"]
200
+ else "[dim]✗[/dim]"
201
+ )
202
+ )
203
+ map_table.add_row(
204
+ row["name"],
205
+ row["transport"].upper(),
206
+ oauth_status,
207
+ persist_disp,
208
+ token_disp,
209
+ row["identity"],
210
+ )
211
+ console.print(map_table)
212
+
213
+ console.print(
214
+ "\n[dim]Run 'fast-agent auth clear --identity <identity>' to remove a token, or 'fast-agent auth clear --all' to remove all.[/dim]"
215
+ )
216
+
217
+
218
+ @app.command()
219
+ def clear(
220
+ server: Optional[str] = typer.Argument(None, help="Server name to clear (from config)"),
221
+ identity: Optional[str] = typer.Option(
222
+ None, "--identity", help="Token identity (base URL) to clear"
223
+ ),
224
+ all: bool = typer.Option(False, "--all", help="Clear tokens for all identities in keyring"),
225
+ config_path: Optional[str] = typer.Option(None, "--config-path", "-c"),
226
+ ) -> None:
227
+ """Clear stored OAuth tokens from the keyring."""
228
+ targets_identities: list[str] = []
229
+ if all:
230
+ targets_identities = list_keyring_tokens()
231
+ elif identity:
232
+ targets_identities = [identity]
233
+ elif server:
234
+ settings = get_settings(config_path)
235
+ rows = _server_rows_from_settings(settings)
236
+ match = next((r for r in rows if r["name"] == server), None)
237
+ if not match:
238
+ typer.echo(f"Server '{server}' not found in config")
239
+ raise typer.Exit(1)
240
+ targets_identities = [match["identity"]]
241
+ else:
242
+ typer.echo("Provide --identity, a server name, or use --all")
243
+ raise typer.Exit(1)
244
+
245
+ # Confirm destructive action
246
+ if not typer.confirm("Remove tokens for the selected server(s) from keyring?", default=False):
247
+ raise typer.Exit()
248
+
249
+ removed_any = False
250
+ for ident in targets_identities:
251
+ if clear_keyring_token(ident):
252
+ removed_any = True
253
+ if removed_any:
254
+ typer.echo("Tokens removed.")
255
+ else:
256
+ typer.echo("No tokens found or nothing removed.")
257
+
258
+
259
+ @app.callback(invoke_without_command=True)
260
+ def main(
261
+ ctx: typer.Context, config_path: Optional[str] = typer.Option(None, "--config-path", "-c")
262
+ ) -> None:
263
+ """Default to showing status if no subcommand is provided."""
264
+ if ctx.invoked_subcommand is None:
265
+ try:
266
+ status(target=None, config_path=config_path)
267
+ except Exception as e:
268
+ typer.echo(f"Error showing auth status: {e}")
269
+
270
+
271
+ @app.command()
272
+ def login(
273
+ target: str = typer.Argument(..., help="Server name (from config) or identity (base URL)"),
274
+ transport: Optional[str] = typer.Option(
275
+ None, "--transport", help="Transport for identity mode: http or sse"
276
+ ),
277
+ config_path: Optional[str] = typer.Option(None, "--config-path", "-c"),
278
+ ) -> None:
279
+ """Start OAuth flow and store tokens for a server.
280
+
281
+ Accepts either a configured server name or an identity (base URL).
282
+ For identity mode, default transport is 'http' (uses <identity>/mcp).
283
+ """
284
+ # Resolve to a minimal MCPServerSettings
285
+ from fast_agent.config import MCPServerAuthSettings, MCPServerSettings
286
+ from fast_agent.mcp.oauth_client import build_oauth_provider
287
+
288
+ cfg = None
289
+ resolved_transport = None
290
+
291
+ if "://" in target:
292
+ # Identity mode
293
+ base = _derive_base_server_url(target)
294
+ if not base:
295
+ typer.echo("Invalid identity URL")
296
+ raise typer.Exit(1)
297
+ resolved_transport = (transport or "http").lower()
298
+ if resolved_transport not in ("http", "sse"):
299
+ typer.echo("--transport must be 'http' or 'sse'")
300
+ raise typer.Exit(1)
301
+ endpoint = base + ("/mcp" if resolved_transport == "http" else "/sse")
302
+ cfg = MCPServerSettings(
303
+ name=base,
304
+ transport=resolved_transport,
305
+ url=endpoint,
306
+ auth=MCPServerAuthSettings(),
307
+ )
308
+ else:
309
+ # Server name mode
310
+ settings = get_settings(config_path)
311
+ servers = getattr(getattr(settings, "mcp", None), "servers", {}) or {}
312
+ cfg = servers.get(target)
313
+ if not cfg:
314
+ typer.echo(f"Server '{target}' not found in config")
315
+ raise typer.Exit(1)
316
+ resolved_transport = getattr(cfg, "transport", "")
317
+ if resolved_transport == "stdio":
318
+ typer.echo("STDIO servers do not support OAuth")
319
+ raise typer.Exit(1)
320
+
321
+ # Build OAuth provider
322
+ provider = build_oauth_provider(cfg)
323
+ if provider is None:
324
+ typer.echo("OAuth is disabled or misconfigured for this server/identity")
325
+ raise typer.Exit(1)
326
+
327
+ async def _run_login():
328
+ try:
329
+ # Use appropriate transport; connect and initialize a minimal session
330
+ if resolved_transport == "http":
331
+ from mcp.client.session import ClientSession
332
+ from mcp.client.streamable_http import streamablehttp_client
333
+
334
+ async with streamablehttp_client(
335
+ cfg.url or "",
336
+ getattr(cfg, "headers", None),
337
+ auth=provider,
338
+ ) as (read_stream, write_stream, _get_session_id):
339
+ async with ClientSession(read_stream, write_stream) as session:
340
+ await session.initialize()
341
+ return True
342
+ elif resolved_transport == "sse":
343
+ from mcp.client.session import ClientSession
344
+ from mcp.client.sse import sse_client
345
+
346
+ async with sse_client(
347
+ cfg.url or "",
348
+ getattr(cfg, "headers", None),
349
+ auth=provider,
350
+ ) as (read_stream, write_stream):
351
+ async with ClientSession(read_stream, write_stream) as session:
352
+ await session.initialize()
353
+ return True
354
+ else:
355
+ return False
356
+ except Exception as e:
357
+ # Surface concise error; detailed logging is in the library
358
+ typer.echo(f"Login failed: {e}")
359
+ return False
360
+
361
+ import asyncio
362
+
363
+ ok = asyncio.run(_run_login())
364
+ if ok:
365
+ from fast_agent.mcp.oauth_client import compute_server_identity
366
+
367
+ ident = compute_server_identity(cfg)
368
+ typer.echo(f"Authenticated. Tokens stored for identity: {ident}")
369
+ else:
370
+ raise typer.Exit(1)
@@ -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,16 @@ 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
+ try:
309
+ import keyring # type: ignore
310
+
311
+ keyring_backend = keyring.get_keyring()
312
+ keyring_name = getattr(keyring_backend, "name", keyring_backend.__class__.__name__)
313
+ except Exception:
314
+ keyring = None # type: ignore
315
+ keyring_name = "unavailable"
316
+
308
317
  # Python info (highlight version and path in green)
309
318
  env_table.add_row(
310
319
  "Python Version", f"[green]{'.'.join(system_info['python_version'].split('.')[:3])}[/green]"
@@ -339,6 +348,9 @@ def show_check_summary() -> None:
339
348
  default_model_value = config_summary.get("default_model", "haiku (system default)")
340
349
  env_table.add_row("Default Model", f"[green]{default_model_value}[/green]")
341
350
 
351
+ # Keyring backend (always shown in application-level settings)
352
+ env_table.add_row("Keyring Backend", f"[green]{keyring_name}[/green]")
353
+
342
354
  console.print(env_table)
343
355
 
344
356
  # Logger Settings panel with two-column layout
@@ -470,10 +482,15 @@ def show_check_summary() -> None:
470
482
  if config_summary.get("status") == "parsed":
471
483
  mcp_servers = config_summary.get("mcp_servers", [])
472
484
  if mcp_servers:
485
+ from fast_agent.config import MCPServerSettings
486
+ from fast_agent.mcp.oauth_client import compute_server_identity
487
+
473
488
  servers_table = Table(show_header=True, box=None)
474
489
  servers_table.add_column("Name", style="white", header_style="bold bright_white")
475
490
  servers_table.add_column("Transport", style="white", header_style="bold bright_white")
476
491
  servers_table.add_column("Command/URL", header_style="bold bright_white")
492
+ servers_table.add_column("OAuth", header_style="bold bright_white")
493
+ servers_table.add_column("Token", header_style="bold bright_white")
477
494
 
478
495
  for server in mcp_servers:
479
496
  name = server["name"]
@@ -489,7 +506,41 @@ def show_check_summary() -> None:
489
506
  if "Not configured" not in command_url:
490
507
  command_url = f"[green]{command_url}[/green]"
491
508
 
492
- servers_table.add_row(name, transport, command_url)
509
+ # OAuth status and token presence
510
+ # Default for unsupported transports (e.g., STDIO): show "-" rather than "off"
511
+ oauth_status = "[dim]-[/dim]"
512
+ token_status = "[dim]n/a[/dim]"
513
+ # Attempt to reconstruct minimal server settings for identity check
514
+ try:
515
+ cfg = MCPServerSettings(
516
+ name=name,
517
+ transport="sse" if transport == "SSE" else ("stdio" if transport == "STDIO" else "http"),
518
+ url=(server.get("url") or None),
519
+ auth=server.get("auth") if isinstance(server.get("auth"), dict) else None,
520
+ )
521
+ except Exception:
522
+ cfg = None
523
+
524
+ if cfg and cfg.transport in ("http", "sse"):
525
+ # Determine if OAuth is enabled for this server
526
+ oauth_enabled = True
527
+ if cfg.auth is not None and hasattr(cfg.auth, "oauth"):
528
+ oauth_enabled = bool(getattr(cfg.auth, "oauth"))
529
+ oauth_status = "[green]on[/green]" if oauth_enabled else "[dim]off[/dim]"
530
+
531
+ # Only check token presence when using keyring persist
532
+ persist = "keyring"
533
+ if cfg.auth is not None and hasattr(cfg.auth, "persist"):
534
+ persist = getattr(cfg.auth, "persist") or "keyring"
535
+ if keyring and persist == "keyring" and oauth_enabled:
536
+ identity = compute_server_identity(cfg)
537
+ tkey = f"oauth:tokens:{identity}"
538
+ has = keyring.get_password("fast-agent-mcp", tkey) is not None
539
+ token_status = "[bold green]✓[/bold green]" if has else "[dim]✗[/dim]"
540
+ elif persist == "memory" and oauth_enabled:
541
+ token_status = "[yellow]memory[/yellow]"
542
+
543
+ servers_table.add_row(name, transport, command_url, oauth_status, token_status)
493
544
 
494
545
  _print_section_header("MCP Servers", color="blue")
495
546
  console.print(servers_table)
@@ -7,10 +7,10 @@ from typing import Dict, List, Optional
7
7
 
8
8
  import typer
9
9
 
10
+ from fast_agent import FastAgent
10
11
  from fast_agent.agents.llm_agent import LlmAgent
11
12
  from fast_agent.cli.commands.server_helpers import add_servers_to_config, generate_server_name
12
13
  from fast_agent.cli.commands.url_parser import generate_server_configs, parse_server_urls
13
- from fast_agent.core.fastagent import FastAgent
14
14
  from fast_agent.ui.console_display import ConsoleDisplay
15
15
 
16
16
  app = typer.Typer(
@@ -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 = 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
- fast_app.app.context.config.mcp.servers[server_name] = MCPServerSettings(**server_settings)
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