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.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- 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)
|