fast-agent-mcp 0.4.7__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.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,404 @@
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
+ import typer
9
+ from rich.table import Table
10
+
11
+ from fast_agent.config import Settings, get_settings
12
+ from fast_agent.mcp.oauth_client import (
13
+ _derive_base_server_url,
14
+ clear_keyring_token,
15
+ compute_server_identity,
16
+ list_keyring_tokens,
17
+ )
18
+ from fast_agent.ui.console import console
19
+
20
+ app = typer.Typer(help="Manage OAuth authentication state for MCP servers")
21
+
22
+
23
+ def _get_keyring_status() -> tuple[str, bool]:
24
+ """Return (backend_name, usable) where usable=False for the fail backend or missing keyring."""
25
+ try:
26
+ import keyring
27
+
28
+ kr = keyring.get_keyring()
29
+ name = getattr(kr, "name", kr.__class__.__name__)
30
+ try:
31
+ from keyring.backends.fail import Keyring as FailKeyring # type: ignore
32
+
33
+ return name, not isinstance(kr, FailKeyring)
34
+ except Exception:
35
+ # If fail backend marker cannot be imported, assume usable
36
+ return name, True
37
+ except Exception:
38
+ return "unavailable", False
39
+
40
+
41
+ def _get_keyring_backend_name() -> str:
42
+ # Backwards-compat helper; prefer _get_keyring_status in new code
43
+ name, _ = _get_keyring_status()
44
+ return name
45
+
46
+
47
+ def _keyring_get_password(service: str, username: str) -> str | None:
48
+ try:
49
+ import keyring
50
+
51
+ return keyring.get_password(service, username)
52
+ except Exception:
53
+ return None
54
+
55
+
56
+ def _keyring_delete_password(service: str, username: str) -> bool:
57
+ try:
58
+ import keyring
59
+
60
+ keyring.delete_password(service, username)
61
+ return True
62
+ except Exception:
63
+ return False
64
+
65
+
66
+ def _server_rows_from_settings(settings: Settings):
67
+ rows = []
68
+ mcp = getattr(settings, "mcp", None)
69
+ servers = getattr(mcp, "servers", {}) if mcp else {}
70
+ for name, cfg in servers.items():
71
+ transport = getattr(cfg, "transport", "")
72
+ if transport == "stdio":
73
+ # STDIO servers do not use OAuth; skip in auth views
74
+ continue
75
+ url = getattr(cfg, "url", None)
76
+ auth = getattr(cfg, "auth", None)
77
+ oauth_enabled = getattr(auth, "oauth", True) if auth is not None else True
78
+ persist = getattr(auth, "persist", "keyring") if auth is not None else "keyring"
79
+ identity = compute_server_identity(cfg)
80
+ # token presence only meaningful if persist is keyring and transport is http/sse
81
+ has_token = False
82
+ if persist == "keyring" and transport in ("http", "sse") and oauth_enabled:
83
+ has_token = (
84
+ _keyring_get_password("fast-agent-mcp", f"oauth:tokens:{identity}") is not None
85
+ )
86
+ rows.append(
87
+ {
88
+ "name": name,
89
+ "transport": transport,
90
+ "url": url or "",
91
+ "persist": persist,
92
+ "oauth": oauth_enabled and transport in ("http", "sse"),
93
+ "has_token": has_token,
94
+ "identity": identity,
95
+ }
96
+ )
97
+ return rows
98
+
99
+
100
+ def _servers_by_identity(settings: Settings) -> dict[str, list[str]]:
101
+ """Group configured server names by derived identity (base URL)."""
102
+ mapping: dict[str, list[str]] = {}
103
+ mcp = getattr(settings, "mcp", None)
104
+ servers = getattr(mcp, "servers", {}) if mcp else {}
105
+ for name, cfg in servers.items():
106
+ try:
107
+ identity = compute_server_identity(cfg)
108
+ except Exception:
109
+ identity = name
110
+ mapping.setdefault(identity, []).append(name)
111
+ return mapping
112
+
113
+
114
+ @app.command()
115
+ def status(
116
+ target: str | None = typer.Argument(None, help="Identity (base URL) or server name"),
117
+ config_path: str | None = typer.Option(None, "--config-path", "-c"),
118
+ ) -> None:
119
+ """Show keyring backend and token status for configured MCP servers."""
120
+ settings = get_settings(config_path)
121
+ backend, backend_usable = _get_keyring_status()
122
+
123
+ # Single-target view if target provided
124
+ if target:
125
+ settings = get_settings(config_path)
126
+ identity = _derive_base_server_url(target) if "://" in target else None
127
+ if not identity:
128
+ servers = getattr(getattr(settings, "mcp", None), "servers", {}) or {}
129
+ cfg = servers.get(target)
130
+ if not cfg:
131
+ typer.echo(f"Server '{target}' not found in config; treating as identity")
132
+ identity = target
133
+ else:
134
+ identity = compute_server_identity(cfg)
135
+
136
+ # Direct presence check
137
+ present = False
138
+ if backend_usable:
139
+ try:
140
+ import keyring
141
+
142
+ present = (
143
+ keyring.get_password("fast-agent-mcp", f"oauth:tokens:{identity}") is not None
144
+ )
145
+ except Exception:
146
+ present = False
147
+
148
+ table = Table(show_header=True, box=None)
149
+ table.add_column("Identity", header_style="bold")
150
+ table.add_column("Token", header_style="bold")
151
+ table.add_column("Servers", header_style="bold")
152
+ by_id = _servers_by_identity(settings)
153
+ servers_for_id = ", ".join(by_id.get(identity, [])) or "[dim]None[/dim]"
154
+ token_disp = "[bold green]✓[/bold green]" if present else "[dim]✗[/dim]"
155
+ table.add_row(identity, token_disp, servers_for_id)
156
+
157
+ if backend_usable and backend != "unavailable":
158
+ console.print(f"Keyring backend: [green]{backend}[/green]")
159
+ else:
160
+ console.print("Keyring backend: [red]not available[/red]")
161
+ console.print(table)
162
+ console.print(
163
+ "\n[dim]Run 'fast-agent auth clear --identity "
164
+ f"{identity}[/dim][dim]' to remove this token, or 'fast-agent auth clear --all' to remove all.[/dim]"
165
+ )
166
+ return
167
+
168
+ # Full status view
169
+ if backend_usable and backend != "unavailable":
170
+ console.print(f"Keyring backend: [green]{backend}[/green]")
171
+ else:
172
+ console.print("Keyring backend: [red]not available[/red]")
173
+
174
+ tokens = list_keyring_tokens()
175
+ token_table = Table(show_header=True, box=None)
176
+ token_table.add_column("Stored Tokens (Identity)", header_style="bold")
177
+ token_table.add_column("Present", header_style="bold")
178
+ if tokens:
179
+ for ident in tokens:
180
+ token_table.add_row(ident, "[bold green]✓[/bold green]")
181
+ else:
182
+ token_table.add_row("[dim]None[/dim]", "[dim]✗[/dim]")
183
+
184
+ console.print(token_table)
185
+
186
+ rows = _server_rows_from_settings(settings)
187
+ if rows:
188
+ map_table = Table(show_header=True, box=None)
189
+ map_table.add_column("Server", header_style="bold")
190
+ map_table.add_column("Transport", header_style="bold")
191
+ map_table.add_column("OAuth", header_style="bold")
192
+ map_table.add_column("Persist", header_style="bold")
193
+ map_table.add_column("Token", header_style="bold")
194
+ map_table.add_column("Identity", header_style="bold")
195
+ for row in rows:
196
+ oauth_status = "[green]on[/green]" if row["oauth"] else "[dim]off[/dim]"
197
+ persist = row["persist"]
198
+ persist_disp = (
199
+ f"[green]{persist}[/green]"
200
+ if persist == "keyring"
201
+ else f"[yellow]{persist}[/yellow]"
202
+ )
203
+ # Direct presence check for each identity so status works even without index
204
+ has_token = False
205
+ token_disp = "[dim]✗[/dim]"
206
+ if persist == "keyring" and row["oauth"]:
207
+ if backend_usable:
208
+ try:
209
+ import keyring
210
+
211
+ has_token = (
212
+ keyring.get_password(
213
+ "fast-agent-mcp", f"oauth:tokens:{row['identity']}"
214
+ )
215
+ is not None
216
+ )
217
+ except Exception:
218
+ has_token = False
219
+ token_disp = "[bold green]✓[/bold green]" if has_token else "[dim]✗[/dim]"
220
+ else:
221
+ token_disp = "[red]not available[/red]"
222
+ elif persist == "memory" and row["oauth"]:
223
+ token_disp = "[yellow]memory[/yellow]"
224
+ map_table.add_row(
225
+ row["name"],
226
+ row["transport"].upper(),
227
+ oauth_status,
228
+ persist_disp,
229
+ token_disp,
230
+ row["identity"],
231
+ )
232
+ console.print(map_table)
233
+
234
+ console.print(
235
+ "\n[dim]Run 'fast-agent auth clear --identity <identity>' to remove a token, or 'fast-agent auth clear --all' to remove all.[/dim]"
236
+ )
237
+
238
+
239
+ @app.command()
240
+ def clear(
241
+ server: str | None = typer.Argument(None, help="Server name to clear (from config)"),
242
+ identity: str | None = typer.Option(
243
+ None, "--identity", help="Token identity (base URL) to clear"
244
+ ),
245
+ all: bool = typer.Option(False, "--all", help="Clear tokens for all identities in keyring"),
246
+ config_path: str | None = typer.Option(None, "--config-path", "-c"),
247
+ ) -> None:
248
+ """Clear stored OAuth tokens from the keyring."""
249
+ targets_identities: list[str] = []
250
+ if all:
251
+ targets_identities = list_keyring_tokens()
252
+ elif identity:
253
+ targets_identities = [identity]
254
+ elif server:
255
+ settings = get_settings(config_path)
256
+ rows = _server_rows_from_settings(settings)
257
+ match = next((r for r in rows if r["name"] == server), None)
258
+ if not match:
259
+ typer.echo(f"Server '{server}' not found in config")
260
+ raise typer.Exit(1)
261
+ targets_identities = [match["identity"]]
262
+ else:
263
+ typer.echo("Provide --identity, a server name, or use --all")
264
+ raise typer.Exit(1)
265
+
266
+ # Confirm destructive action
267
+ if not typer.confirm("Remove tokens for the selected server(s) from keyring?", default=False):
268
+ raise typer.Exit()
269
+
270
+ removed_any = False
271
+ for ident in targets_identities:
272
+ if clear_keyring_token(ident):
273
+ removed_any = True
274
+ if removed_any:
275
+ typer.echo("Tokens removed.")
276
+ else:
277
+ typer.echo("No tokens found or nothing removed.")
278
+
279
+
280
+ @app.callback(invoke_without_command=True)
281
+ def main(
282
+ ctx: typer.Context, config_path: str | None = typer.Option(None, "--config-path", "-c")
283
+ ) -> None:
284
+ """Default to showing status if no subcommand is provided."""
285
+ if ctx.invoked_subcommand is None:
286
+ try:
287
+ status(target=None, config_path=config_path)
288
+ except Exception as e:
289
+ typer.echo(f"Error showing auth status: {e}")
290
+
291
+
292
+ @app.command()
293
+ def login(
294
+ target: str | None = typer.Argument(
295
+ None, help="Server name (from config) or identity (base URL)"
296
+ ),
297
+ transport: str | None = typer.Option(
298
+ None, "--transport", help="Transport for identity mode: http or sse"
299
+ ),
300
+ config_path: str | None = typer.Option(None, "--config-path", "-c"),
301
+ ) -> None:
302
+ """Start OAuth flow and store tokens for a server.
303
+
304
+ Accepts either a configured server name or an identity (base URL).
305
+ For identity mode, default transport is 'http' (uses <identity>/mcp).
306
+ """
307
+ # Resolve to a minimal MCPServerSettings
308
+ from fast_agent.config import MCPServerAuthSettings, MCPServerSettings
309
+ from fast_agent.mcp.oauth_client import build_oauth_provider
310
+
311
+ cfg = None
312
+ resolved_transport = None
313
+
314
+ if target is None or not target.strip():
315
+ typer.echo("Provide a server name or identity URL to log in.")
316
+ typer.echo(
317
+ "Example: `fast-agent auth login my-server` "
318
+ "or `fast-agent auth login https://example.com`."
319
+ )
320
+ typer.echo("Run `fast-agent auth login --help` for more details.")
321
+ raise typer.Exit(1)
322
+
323
+ target = target.strip()
324
+
325
+ if "://" in target:
326
+ # Identity mode
327
+ base = _derive_base_server_url(target)
328
+ if not base:
329
+ typer.echo("Invalid identity URL")
330
+ raise typer.Exit(1)
331
+ resolved_transport = (transport or "http").lower()
332
+ if resolved_transport not in ("http", "sse"):
333
+ typer.echo("--transport must be 'http' or 'sse'")
334
+ raise typer.Exit(1)
335
+ endpoint = base + ("/mcp" if resolved_transport == "http" else "/sse")
336
+ cfg = MCPServerSettings(
337
+ name=base,
338
+ transport=resolved_transport,
339
+ url=endpoint,
340
+ auth=MCPServerAuthSettings(),
341
+ )
342
+ else:
343
+ # Server name mode
344
+ settings = get_settings(config_path)
345
+ servers = getattr(getattr(settings, "mcp", None), "servers", {}) or {}
346
+ cfg = servers.get(target)
347
+ if not cfg:
348
+ typer.echo(f"Server '{target}' not found in config")
349
+ raise typer.Exit(1)
350
+ resolved_transport = getattr(cfg, "transport", "")
351
+ if resolved_transport == "stdio":
352
+ typer.echo("STDIO servers do not support OAuth")
353
+ raise typer.Exit(1)
354
+
355
+ # Build OAuth provider
356
+ provider = build_oauth_provider(cfg)
357
+ if provider is None:
358
+ typer.echo("OAuth is disabled or misconfigured for this server/identity")
359
+ raise typer.Exit(1)
360
+
361
+ async def _run_login():
362
+ try:
363
+ # Use appropriate transport; connect and initialize a minimal session
364
+ if resolved_transport == "http":
365
+ from mcp.client.session import ClientSession
366
+ from mcp.client.streamable_http import streamablehttp_client
367
+
368
+ async with streamablehttp_client(
369
+ cfg.url or "",
370
+ getattr(cfg, "headers", None),
371
+ auth=provider,
372
+ ) as (read_stream, write_stream, _get_session_id):
373
+ async with ClientSession(read_stream, write_stream) as session:
374
+ await session.initialize()
375
+ return True
376
+ elif resolved_transport == "sse":
377
+ from mcp.client.session import ClientSession
378
+ from mcp.client.sse import sse_client
379
+
380
+ async with sse_client(
381
+ cfg.url or "",
382
+ getattr(cfg, "headers", None),
383
+ auth=provider,
384
+ ) as (read_stream, write_stream):
385
+ async with ClientSession(read_stream, write_stream) as session:
386
+ await session.initialize()
387
+ return True
388
+ else:
389
+ return False
390
+ except Exception as e:
391
+ # Surface concise error; detailed logging is in the library
392
+ typer.echo(f"Login failed: {e}")
393
+ return False
394
+
395
+ import asyncio
396
+
397
+ ok = asyncio.run(_run_login())
398
+ if ok:
399
+ from fast_agent.mcp.oauth_client import compute_server_identity
400
+
401
+ ident = compute_server_identity(cfg)
402
+ typer.echo(f"Authenticated. Tokens stored for identity: {ident}")
403
+ else:
404
+ raise typer.Exit(1)