fast-agent-mcp 0.3.4__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.

@@ -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)
@@ -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
@@ -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 = Console()
9
+ console = shared_console
9
10
 
10
11
 
11
12
  def load_template_text(filename: str) -> str:
@@ -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