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.
- fast_agent/cli/__main__.py +8 -5
- fast_agent/cli/commands/auth.py +370 -0
- fast_agent/cli/commands/check_config.py +54 -3
- fast_agent/cli/commands/quickstart.py +3 -1
- fast_agent/cli/commands/server_helpers.py +10 -2
- fast_agent/cli/commands/setup.py +3 -2
- fast_agent/cli/constants.py +1 -1
- fast_agent/cli/main.py +3 -1
- fast_agent/config.py +59 -8
- fast_agent/mcp/mcp_connection_manager.py +21 -3
- fast_agent/mcp/oauth_client.py +481 -0
- fast_agent/resources/setup/fastagent.config.yaml +1 -2
- {fast_agent_mcp-0.3.4.dist-info → fast_agent_mcp-0.3.5.dist-info}/METADATA +39 -2
- {fast_agent_mcp-0.3.4.dist-info → fast_agent_mcp-0.3.5.dist-info}/RECORD +17 -15
- {fast_agent_mcp-0.3.4.dist-info → fast_agent_mcp-0.3.5.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.4.dist-info → fast_agent_mcp-0.3.5.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.4.dist-info → fast_agent_mcp-0.3.5.dist-info}/licenses/LICENSE +0 -0
fast_agent/cli/__main__.py
CHANGED
|
@@ -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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
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
|
|