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,783 @@
|
|
|
1
|
+
"""Command to check FastAgent configuration."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import sys
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
import yaml
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from fast_agent.llm.provider_key_manager import API_KEY_HINT_TEXT, ProviderKeyManager
|
|
14
|
+
from fast_agent.llm.provider_types import Provider
|
|
15
|
+
from fast_agent.skills import SkillRegistry
|
|
16
|
+
from fast_agent.ui.console import console
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(
|
|
19
|
+
help="Check and diagnose FastAgent configuration",
|
|
20
|
+
no_args_is_help=False, # Allow showing our custom help instead
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def find_config_files(start_path: Path) -> dict[str, Path | None]:
|
|
25
|
+
"""Find FastAgent configuration files, preferring secrets file next to config file."""
|
|
26
|
+
from fast_agent.config import find_fastagent_config_files
|
|
27
|
+
|
|
28
|
+
config_path, secrets_path = find_fastagent_config_files(start_path)
|
|
29
|
+
return {
|
|
30
|
+
"config": config_path,
|
|
31
|
+
"secrets": secrets_path,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_system_info() -> dict:
|
|
36
|
+
"""Get system information including Python version, OS, etc."""
|
|
37
|
+
return {
|
|
38
|
+
"platform": platform.system(),
|
|
39
|
+
"platform_version": platform.version(),
|
|
40
|
+
"python_version": sys.version,
|
|
41
|
+
"python_path": sys.executable,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_secrets_summary(secrets_path: Path | None) -> dict:
|
|
46
|
+
"""Extract information from the secrets file."""
|
|
47
|
+
result = {
|
|
48
|
+
"status": "not_found", # Default status: not found
|
|
49
|
+
"error": None,
|
|
50
|
+
"secrets": {},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if not secrets_path:
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
if not secrets_path.exists():
|
|
57
|
+
result["status"] = "not_found"
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
# File exists, attempt to parse
|
|
61
|
+
try:
|
|
62
|
+
with open(secrets_path, "r") as f:
|
|
63
|
+
secrets = yaml.safe_load(f)
|
|
64
|
+
|
|
65
|
+
# Mark as successfully parsed
|
|
66
|
+
result["status"] = "parsed"
|
|
67
|
+
result["secrets"] = secrets or {}
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
# File exists but has parse errors
|
|
71
|
+
result["status"] = "error"
|
|
72
|
+
result["error"] = str(e)
|
|
73
|
+
console.print(f"[yellow]Warning:[/yellow] Error parsing secrets file: {e}")
|
|
74
|
+
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def check_api_keys(secrets_summary: dict, config_summary: dict) -> dict:
|
|
79
|
+
"""Check if API keys are configured in secrets file or environment, including Azure DefaultAzureCredential.
|
|
80
|
+
Now also checks Azure config in main config file for retrocompatibility.
|
|
81
|
+
"""
|
|
82
|
+
import os
|
|
83
|
+
|
|
84
|
+
results = {
|
|
85
|
+
provider.value: {"env": "", "config": ""}
|
|
86
|
+
for provider in Provider
|
|
87
|
+
if provider != Provider.FAST_AGENT
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Get secrets if available
|
|
91
|
+
secrets = secrets_summary.get("secrets", {})
|
|
92
|
+
secrets_status = secrets_summary.get("status", "not_found")
|
|
93
|
+
# Get config if available
|
|
94
|
+
config = config_summary if config_summary.get("status") == "parsed" else {}
|
|
95
|
+
|
|
96
|
+
config_azure = {}
|
|
97
|
+
if config and "azure" in config.get("config", {}):
|
|
98
|
+
config_azure = config["config"]["azure"]
|
|
99
|
+
|
|
100
|
+
for provider in results:
|
|
101
|
+
# Always check environment variables first
|
|
102
|
+
env_key_name = ProviderKeyManager.get_env_key_name(provider)
|
|
103
|
+
env_key_value = os.environ.get(env_key_name)
|
|
104
|
+
if env_key_value:
|
|
105
|
+
if len(env_key_value) > 5:
|
|
106
|
+
results[provider]["env"] = f"...{env_key_value[-5:]}"
|
|
107
|
+
else:
|
|
108
|
+
results[provider]["env"] = "...***"
|
|
109
|
+
|
|
110
|
+
# Special handling for Azure: support api_key and DefaultAzureCredential
|
|
111
|
+
if provider == "azure":
|
|
112
|
+
# Prefer secrets if present, else fallback to config
|
|
113
|
+
azure_cfg = {}
|
|
114
|
+
if secrets_status == "parsed" and "azure" in secrets:
|
|
115
|
+
azure_cfg = secrets.get("azure", {})
|
|
116
|
+
elif config_azure:
|
|
117
|
+
azure_cfg = config_azure
|
|
118
|
+
|
|
119
|
+
use_default_cred = azure_cfg.get("use_default_azure_credential", False)
|
|
120
|
+
base_url = azure_cfg.get("base_url")
|
|
121
|
+
if use_default_cred and base_url:
|
|
122
|
+
results[provider]["config"] = "DefaultAzureCredential"
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# Check secrets file if it was parsed successfully
|
|
126
|
+
if secrets_status == "parsed":
|
|
127
|
+
config_key = ProviderKeyManager.get_config_file_key(provider, secrets)
|
|
128
|
+
if config_key and config_key != API_KEY_HINT_TEXT:
|
|
129
|
+
if len(config_key) > 5:
|
|
130
|
+
results[provider]["config"] = f"...{config_key[-5:]}"
|
|
131
|
+
else:
|
|
132
|
+
results[provider]["config"] = "...***"
|
|
133
|
+
|
|
134
|
+
return results
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_fastagent_version() -> str:
|
|
138
|
+
"""Get the installed version of FastAgent."""
|
|
139
|
+
try:
|
|
140
|
+
return version("fast-agent-mcp")
|
|
141
|
+
except: # noqa: E722
|
|
142
|
+
return "unknown"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_config_summary(config_path: Path | None) -> dict:
|
|
146
|
+
"""Extract key information from the configuration file."""
|
|
147
|
+
from fast_agent.config import MCPTimelineSettings, Settings
|
|
148
|
+
|
|
149
|
+
# Get actual defaults from Settings class
|
|
150
|
+
default_settings = Settings()
|
|
151
|
+
|
|
152
|
+
result = {
|
|
153
|
+
"status": "not_found", # Default status: not found
|
|
154
|
+
"error": None,
|
|
155
|
+
"default_model": default_settings.default_model,
|
|
156
|
+
"logger": {
|
|
157
|
+
"level": default_settings.logger.level,
|
|
158
|
+
"type": default_settings.logger.type,
|
|
159
|
+
"streaming": default_settings.logger.streaming,
|
|
160
|
+
"progress_display": default_settings.logger.progress_display,
|
|
161
|
+
"show_chat": default_settings.logger.show_chat,
|
|
162
|
+
"show_tools": default_settings.logger.show_tools,
|
|
163
|
+
"truncate_tools": default_settings.logger.truncate_tools,
|
|
164
|
+
"enable_markup": default_settings.logger.enable_markup,
|
|
165
|
+
},
|
|
166
|
+
"mcp_ui_mode": default_settings.mcp_ui_mode,
|
|
167
|
+
"timeline": {
|
|
168
|
+
"steps": default_settings.mcp_timeline.steps,
|
|
169
|
+
"step_seconds": default_settings.mcp_timeline.step_seconds,
|
|
170
|
+
},
|
|
171
|
+
"mcp_servers": [],
|
|
172
|
+
"skills_directory": None,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if not config_path:
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
if not config_path.exists():
|
|
179
|
+
result["status"] = "not_found"
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
# File exists, attempt to parse
|
|
183
|
+
try:
|
|
184
|
+
with open(config_path, "r") as f:
|
|
185
|
+
config = yaml.safe_load(f)
|
|
186
|
+
|
|
187
|
+
# Mark as successfully parsed
|
|
188
|
+
result["status"] = "parsed"
|
|
189
|
+
|
|
190
|
+
if not config:
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
# Get default model
|
|
194
|
+
if "default_model" in config:
|
|
195
|
+
result["default_model"] = config["default_model"]
|
|
196
|
+
|
|
197
|
+
# Get logger settings
|
|
198
|
+
if "logger" in config:
|
|
199
|
+
logger_config = config["logger"]
|
|
200
|
+
result["logger"] = {
|
|
201
|
+
"level": logger_config.get("level", default_settings.logger.level),
|
|
202
|
+
"type": logger_config.get("type", default_settings.logger.type),
|
|
203
|
+
"streaming": logger_config.get("streaming", default_settings.logger.streaming),
|
|
204
|
+
"progress_display": logger_config.get(
|
|
205
|
+
"progress_display", default_settings.logger.progress_display
|
|
206
|
+
),
|
|
207
|
+
"show_chat": logger_config.get("show_chat", default_settings.logger.show_chat),
|
|
208
|
+
"show_tools": logger_config.get("show_tools", default_settings.logger.show_tools),
|
|
209
|
+
"truncate_tools": logger_config.get(
|
|
210
|
+
"truncate_tools", default_settings.logger.truncate_tools
|
|
211
|
+
),
|
|
212
|
+
"enable_markup": logger_config.get(
|
|
213
|
+
"enable_markup", default_settings.logger.enable_markup
|
|
214
|
+
),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# Get MCP UI mode
|
|
218
|
+
if "mcp_ui_mode" in config:
|
|
219
|
+
result["mcp_ui_mode"] = config["mcp_ui_mode"]
|
|
220
|
+
|
|
221
|
+
# Get timeline settings
|
|
222
|
+
if "mcp_timeline" in config:
|
|
223
|
+
try:
|
|
224
|
+
timeline_override = MCPTimelineSettings(**(config.get("mcp_timeline") or {}))
|
|
225
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
226
|
+
console.print(
|
|
227
|
+
"[yellow]Warning:[/yellow] Invalid mcp_timeline configuration; using defaults."
|
|
228
|
+
)
|
|
229
|
+
console.print(f"[yellow]Details:[/yellow] {exc}")
|
|
230
|
+
else:
|
|
231
|
+
result["timeline"] = {
|
|
232
|
+
"steps": timeline_override.steps,
|
|
233
|
+
"step_seconds": timeline_override.step_seconds,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Get MCP server info
|
|
237
|
+
if "mcp" in config and "servers" in config["mcp"]:
|
|
238
|
+
for server_name, server_config in config["mcp"]["servers"].items():
|
|
239
|
+
server_info = {
|
|
240
|
+
"name": server_name,
|
|
241
|
+
"transport": "STDIO", # Default transport type
|
|
242
|
+
"command": "",
|
|
243
|
+
"url": "",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Determine transport type
|
|
247
|
+
if "url" in server_config:
|
|
248
|
+
url = server_config.get("url", "")
|
|
249
|
+
server_info["url"] = url
|
|
250
|
+
|
|
251
|
+
# Use URL path to determine transport type
|
|
252
|
+
try:
|
|
253
|
+
from .url_parser import parse_server_url
|
|
254
|
+
|
|
255
|
+
_, transport_type, _ = parse_server_url(url)
|
|
256
|
+
server_info["transport"] = transport_type.upper()
|
|
257
|
+
except Exception:
|
|
258
|
+
# Fallback to HTTP if URL parsing fails
|
|
259
|
+
server_info["transport"] = "HTTP"
|
|
260
|
+
|
|
261
|
+
# Get command and args
|
|
262
|
+
command = server_config.get("command", "")
|
|
263
|
+
args = server_config.get("args", [])
|
|
264
|
+
|
|
265
|
+
if command:
|
|
266
|
+
if args:
|
|
267
|
+
args_str = " ".join([str(arg) for arg in args])
|
|
268
|
+
full_cmd = f"{command} {args_str}"
|
|
269
|
+
# Truncate if too long
|
|
270
|
+
if len(full_cmd) > 60:
|
|
271
|
+
full_cmd = full_cmd[:57] + "..."
|
|
272
|
+
server_info["command"] = full_cmd
|
|
273
|
+
else:
|
|
274
|
+
server_info["command"] = command
|
|
275
|
+
|
|
276
|
+
# Truncate URL if too long
|
|
277
|
+
if server_info["url"] and len(server_info["url"]) > 60:
|
|
278
|
+
server_info["url"] = server_info["url"][:57] + "..."
|
|
279
|
+
|
|
280
|
+
result["mcp_servers"].append(server_info)
|
|
281
|
+
|
|
282
|
+
# Skills directory override
|
|
283
|
+
skills_cfg = config.get("skills") if isinstance(config, dict) else None
|
|
284
|
+
if isinstance(skills_cfg, dict):
|
|
285
|
+
directory_value = skills_cfg.get("directory")
|
|
286
|
+
if isinstance(directory_value, str) and directory_value.strip():
|
|
287
|
+
result["skills_directory"] = directory_value.strip()
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
# File exists but has parse errors
|
|
291
|
+
result["status"] = "error"
|
|
292
|
+
result["error"] = str(e)
|
|
293
|
+
console.print(f"[red]Error parsing configuration file:[/red] {e}")
|
|
294
|
+
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def show_check_summary() -> None:
|
|
299
|
+
"""Show a summary of checks with colorful styling."""
|
|
300
|
+
cwd = Path.cwd()
|
|
301
|
+
config_files = find_config_files(cwd)
|
|
302
|
+
system_info = get_system_info()
|
|
303
|
+
config_summary = get_config_summary(config_files["config"])
|
|
304
|
+
secrets_summary = get_secrets_summary(config_files["secrets"])
|
|
305
|
+
api_keys = check_api_keys(secrets_summary, config_summary)
|
|
306
|
+
fastagent_version = get_fastagent_version()
|
|
307
|
+
|
|
308
|
+
# Helper to print section headers using the new console_display style
|
|
309
|
+
def _print_section_header(title: str, color: str = "blue") -> None:
|
|
310
|
+
width = console.size.width
|
|
311
|
+
left = f"[{color}]▎[/{color}][dim {color}]▶[/dim {color}] [{color}]{title}[/{color}]"
|
|
312
|
+
left_text = Text.from_markup(left)
|
|
313
|
+
separator_count = max(1, width - left_text.cell_len - 1)
|
|
314
|
+
|
|
315
|
+
combined = Text()
|
|
316
|
+
combined.append_text(left_text)
|
|
317
|
+
combined.append(" ")
|
|
318
|
+
combined.append("─" * separator_count, style="dim")
|
|
319
|
+
|
|
320
|
+
console.print()
|
|
321
|
+
console.print(combined)
|
|
322
|
+
console.print()
|
|
323
|
+
|
|
324
|
+
# Environment and configuration section (merged)
|
|
325
|
+
# Header shows version and platform for a concise overview
|
|
326
|
+
header_title = f"fast-agent v{fastagent_version} ({system_info['platform']})"
|
|
327
|
+
_print_section_header(header_title, color="blue")
|
|
328
|
+
|
|
329
|
+
config_path = config_files["config"]
|
|
330
|
+
secrets_path = config_files["secrets"]
|
|
331
|
+
|
|
332
|
+
env_table = Table(show_header=False, box=None)
|
|
333
|
+
env_table.add_column("Setting", style="white")
|
|
334
|
+
env_table.add_column("Value")
|
|
335
|
+
|
|
336
|
+
# Determine keyring backend early so it can appear in the top section
|
|
337
|
+
# Also detect whether the backend is actually usable (not the fail backend)
|
|
338
|
+
keyring_usable = False
|
|
339
|
+
try:
|
|
340
|
+
import keyring # type: ignore
|
|
341
|
+
|
|
342
|
+
keyring_backend = keyring.get_keyring()
|
|
343
|
+
keyring_name = getattr(keyring_backend, "name", keyring_backend.__class__.__name__)
|
|
344
|
+
try:
|
|
345
|
+
# Detect the "fail" backend explicitly; it's present but unusable
|
|
346
|
+
from keyring.backends.fail import Keyring as FailKeyring # type: ignore
|
|
347
|
+
|
|
348
|
+
keyring_usable = not isinstance(keyring_backend, FailKeyring)
|
|
349
|
+
except Exception:
|
|
350
|
+
# If we can't import the fail backend marker, assume usable
|
|
351
|
+
keyring_usable = True
|
|
352
|
+
except Exception:
|
|
353
|
+
keyring = None # type: ignore
|
|
354
|
+
keyring_name = "unavailable"
|
|
355
|
+
keyring_usable = False
|
|
356
|
+
|
|
357
|
+
# Python info (highlight version and path in green)
|
|
358
|
+
env_table.add_row(
|
|
359
|
+
"Python Version", f"[green]{'.'.join(system_info['python_version'].split('.')[:3])}[/green]"
|
|
360
|
+
)
|
|
361
|
+
env_table.add_row("Python Path", f"[green]{system_info['python_path']}[/green]")
|
|
362
|
+
|
|
363
|
+
# Secrets file status
|
|
364
|
+
secrets_status = secrets_summary.get("status", "not_found")
|
|
365
|
+
if secrets_status == "not_found":
|
|
366
|
+
env_table.add_row("Secrets File", "[yellow]Not found[/yellow]")
|
|
367
|
+
elif secrets_status == "error":
|
|
368
|
+
env_table.add_row("Secrets File", f"[orange_red1]Errors[/orange_red1] ({secrets_path})")
|
|
369
|
+
env_table.add_row(
|
|
370
|
+
"Secrets Error",
|
|
371
|
+
f"[orange_red1]{secrets_summary.get('error', 'Unknown error')}[/orange_red1]",
|
|
372
|
+
)
|
|
373
|
+
else: # parsed successfully
|
|
374
|
+
env_table.add_row("Secrets File", f"[green]Found[/green] ({secrets_path})")
|
|
375
|
+
|
|
376
|
+
# Config file status
|
|
377
|
+
config_status = config_summary.get("status", "not_found")
|
|
378
|
+
if config_status == "not_found":
|
|
379
|
+
env_table.add_row("Config File", "[red]Not found[/red]")
|
|
380
|
+
elif config_status == "error":
|
|
381
|
+
env_table.add_row("Config File", f"[orange_red1]Errors[/orange_red1] ({config_path})")
|
|
382
|
+
env_table.add_row(
|
|
383
|
+
"Config Error",
|
|
384
|
+
f"[orange_red1]{config_summary.get('error', 'Unknown error')}[/orange_red1]",
|
|
385
|
+
)
|
|
386
|
+
else: # parsed successfully
|
|
387
|
+
env_table.add_row("Config File", f"[green]Found[/green] ({config_path})")
|
|
388
|
+
default_model_value = config_summary.get("default_model", "gpt-5-mini.low (system default)")
|
|
389
|
+
env_table.add_row("Default Model", f"[green]{default_model_value}[/green]")
|
|
390
|
+
|
|
391
|
+
# Keyring backend (always shown in application-level settings)
|
|
392
|
+
if keyring_usable and keyring_name != "unavailable":
|
|
393
|
+
env_table.add_row("Keyring Backend", f"[green]{keyring_name}[/green]")
|
|
394
|
+
else:
|
|
395
|
+
env_table.add_row("Keyring Backend", "[red]not available[/red]")
|
|
396
|
+
|
|
397
|
+
console.print(env_table)
|
|
398
|
+
|
|
399
|
+
def _relative_path(path: Path) -> str:
|
|
400
|
+
try:
|
|
401
|
+
return str(path.relative_to(cwd))
|
|
402
|
+
except ValueError:
|
|
403
|
+
return str(path)
|
|
404
|
+
|
|
405
|
+
skills_override = config_summary.get("skills_directory")
|
|
406
|
+
override_directory = Path(skills_override).expanduser() if skills_override else None
|
|
407
|
+
skills_registry = SkillRegistry(base_dir=cwd, override_directory=override_directory)
|
|
408
|
+
skills_dir = skills_registry.directory
|
|
409
|
+
skills_manifests, skill_errors = skills_registry.load_manifests_with_errors()
|
|
410
|
+
|
|
411
|
+
# Logger Settings panel with two-column layout
|
|
412
|
+
logger = config_summary.get("logger", {})
|
|
413
|
+
logger_table = Table(show_header=True, box=None)
|
|
414
|
+
logger_table.add_column("Setting", style="white", header_style="bold bright_white")
|
|
415
|
+
logger_table.add_column("Value", header_style="bold bright_white")
|
|
416
|
+
logger_table.add_column("Setting", style="white", header_style="bold bright_white")
|
|
417
|
+
logger_table.add_column("Value", header_style="bold bright_white")
|
|
418
|
+
|
|
419
|
+
def bool_to_symbol(value):
|
|
420
|
+
return "[bold green]✓[/bold green]" if value else "[bold red]✗[/bold red]"
|
|
421
|
+
|
|
422
|
+
# Format MCP-UI mode value
|
|
423
|
+
mcp_ui_mode = config_summary.get("mcp_ui_mode", "auto")
|
|
424
|
+
if mcp_ui_mode == "disabled":
|
|
425
|
+
mcp_ui_display = "[dim]disabled[/dim]"
|
|
426
|
+
else:
|
|
427
|
+
mcp_ui_display = f"[green]{mcp_ui_mode}[/green]"
|
|
428
|
+
|
|
429
|
+
timeline_settings = config_summary.get("timeline", {})
|
|
430
|
+
timeline_steps = timeline_settings.get("steps", 20)
|
|
431
|
+
timeline_step_seconds = timeline_settings.get("step_seconds", 30)
|
|
432
|
+
|
|
433
|
+
def format_step_interval(seconds: int) -> str:
|
|
434
|
+
try:
|
|
435
|
+
total = int(seconds)
|
|
436
|
+
except (TypeError, ValueError):
|
|
437
|
+
return str(seconds)
|
|
438
|
+
if total <= 0:
|
|
439
|
+
return "0s"
|
|
440
|
+
if total % 86400 == 0:
|
|
441
|
+
return f"{total // 86400}d"
|
|
442
|
+
if total % 3600 == 0:
|
|
443
|
+
return f"{total // 3600}h"
|
|
444
|
+
if total % 60 == 0:
|
|
445
|
+
return f"{total // 60}m"
|
|
446
|
+
minutes, secs = divmod(total, 60)
|
|
447
|
+
if minutes:
|
|
448
|
+
return f"{minutes}m{secs:02d}s"
|
|
449
|
+
return f"{secs}s"
|
|
450
|
+
|
|
451
|
+
# Prepare all settings as pairs
|
|
452
|
+
settings_data = [
|
|
453
|
+
("Log Level", logger.get("level", "warning (default)")),
|
|
454
|
+
("Log Type", logger.get("type", "file (default)")),
|
|
455
|
+
("MCP-UI", mcp_ui_display),
|
|
456
|
+
("Streaming Mode", f"[green]{logger.get('streaming', 'markdown')}[/green]"),
|
|
457
|
+
("Streaming Display", bool_to_symbol(logger.get("streaming_display", True))),
|
|
458
|
+
("Progress Display", bool_to_symbol(logger.get("progress_display", True))),
|
|
459
|
+
("Show Chat", bool_to_symbol(logger.get("show_chat", True))),
|
|
460
|
+
("Show Tools", bool_to_symbol(logger.get("show_tools", True))),
|
|
461
|
+
("Truncate Tools", bool_to_symbol(logger.get("truncate_tools", True))),
|
|
462
|
+
("Enable Markup", bool_to_symbol(logger.get("enable_markup", True))),
|
|
463
|
+
("Timeline Steps", f"[green]{timeline_steps}[/green]"),
|
|
464
|
+
("Timeline Interval", f"[green]{format_step_interval(timeline_step_seconds)}[/green]"),
|
|
465
|
+
]
|
|
466
|
+
|
|
467
|
+
# Add rows in two-column layout, styling some values in green
|
|
468
|
+
for i in range(0, len(settings_data), 2):
|
|
469
|
+
left_setting, left_value = settings_data[i]
|
|
470
|
+
# Style certain values in green (MCP-UI is already pre-styled)
|
|
471
|
+
if left_setting in ("Log Level", "Log Type"):
|
|
472
|
+
left_value = f"[green]{left_value}[/green]"
|
|
473
|
+
if i + 1 < len(settings_data):
|
|
474
|
+
right_setting, right_value = settings_data[i + 1]
|
|
475
|
+
if right_setting in ("Log Level", "Log Type"):
|
|
476
|
+
right_value = f"[green]{right_value}[/green]"
|
|
477
|
+
logger_table.add_row(left_setting, left_value, right_setting, right_value)
|
|
478
|
+
else:
|
|
479
|
+
# Odd number of settings - fill right column with empty strings
|
|
480
|
+
logger_table.add_row(left_setting, left_value, "", "")
|
|
481
|
+
|
|
482
|
+
_print_section_header("Application Settings", color="blue")
|
|
483
|
+
console.print(logger_table)
|
|
484
|
+
|
|
485
|
+
# API keys panel with two-column layout
|
|
486
|
+
keys_table = Table(show_header=True, box=None)
|
|
487
|
+
keys_table.add_column("Provider", style="white", header_style="bold bright_white")
|
|
488
|
+
keys_table.add_column("Env", justify="center", header_style="bold bright_white")
|
|
489
|
+
keys_table.add_column("Config", justify="center", header_style="bold bright_white")
|
|
490
|
+
keys_table.add_column("Active Key", style="green", header_style="bold bright_white")
|
|
491
|
+
keys_table.add_column("Provider", style="white", header_style="bold bright_white")
|
|
492
|
+
keys_table.add_column("Env", justify="center", header_style="bold bright_white")
|
|
493
|
+
keys_table.add_column("Config", justify="center", header_style="bold bright_white")
|
|
494
|
+
keys_table.add_column("Active Key", style="green", header_style="bold bright_white")
|
|
495
|
+
|
|
496
|
+
def format_provider_row(provider, status):
|
|
497
|
+
"""Format a single provider's status for display."""
|
|
498
|
+
# Environment key indicator
|
|
499
|
+
if status["env"] and status["config"]:
|
|
500
|
+
# Both exist but config takes precedence (env is present but not active)
|
|
501
|
+
env_status = "[yellow]✓[/yellow]"
|
|
502
|
+
elif status["env"]:
|
|
503
|
+
# Only env exists and is active
|
|
504
|
+
env_status = "[bold green]✓[/bold green]"
|
|
505
|
+
else:
|
|
506
|
+
# No env key
|
|
507
|
+
env_status = "[dim]✗[/dim]"
|
|
508
|
+
|
|
509
|
+
# Config file key indicator
|
|
510
|
+
if status["config"]:
|
|
511
|
+
# Config exists and takes precedence (is active)
|
|
512
|
+
config_status = "[bold green]✓[/bold green]"
|
|
513
|
+
else:
|
|
514
|
+
# No config key
|
|
515
|
+
config_status = "[dim]✗[/dim]"
|
|
516
|
+
|
|
517
|
+
# Display active key
|
|
518
|
+
if status["config"]:
|
|
519
|
+
# Config key is active
|
|
520
|
+
active = f"[bold green]{status['config']}[/bold green]"
|
|
521
|
+
elif status["env"]:
|
|
522
|
+
# Env key is active
|
|
523
|
+
active = f"[bold green]{status['env']}[/bold green]"
|
|
524
|
+
elif provider == "generic":
|
|
525
|
+
# Generic provider uses "ollama" as a default when no key is set
|
|
526
|
+
active = "[green]ollama (default)[/green]"
|
|
527
|
+
else:
|
|
528
|
+
# No key available for other providers
|
|
529
|
+
active = "[dim]Not configured[/dim]"
|
|
530
|
+
|
|
531
|
+
# Get the proper display name for the provider
|
|
532
|
+
from fast_agent.llm.provider_types import Provider
|
|
533
|
+
|
|
534
|
+
provider_enum = Provider(provider)
|
|
535
|
+
display_name = provider_enum.display_name
|
|
536
|
+
|
|
537
|
+
return display_name, env_status, config_status, active
|
|
538
|
+
|
|
539
|
+
# Split providers into two columns
|
|
540
|
+
providers_list = list(api_keys.items())
|
|
541
|
+
mid_point = (len(providers_list) + 1) // 2 # Round up for odd numbers
|
|
542
|
+
|
|
543
|
+
for i in range(mid_point):
|
|
544
|
+
# Left column
|
|
545
|
+
left_provider, left_status = providers_list[i]
|
|
546
|
+
left_data = format_provider_row(left_provider, left_status)
|
|
547
|
+
|
|
548
|
+
# Right column (if exists)
|
|
549
|
+
if i + mid_point < len(providers_list):
|
|
550
|
+
right_provider, right_status = providers_list[i + mid_point]
|
|
551
|
+
right_data = format_provider_row(right_provider, right_status)
|
|
552
|
+
# Add row with both columns
|
|
553
|
+
keys_table.add_row(*left_data, *right_data)
|
|
554
|
+
else:
|
|
555
|
+
# Add row with only left column (right column empty)
|
|
556
|
+
keys_table.add_row(*left_data, "", "", "", "")
|
|
557
|
+
|
|
558
|
+
# API Keys section
|
|
559
|
+
_print_section_header("API Keys", color="blue")
|
|
560
|
+
console.print(keys_table)
|
|
561
|
+
|
|
562
|
+
# MCP Servers panel (shown after API Keys)
|
|
563
|
+
if config_summary.get("status") == "parsed":
|
|
564
|
+
mcp_servers = config_summary.get("mcp_servers", [])
|
|
565
|
+
if mcp_servers:
|
|
566
|
+
from fast_agent.config import MCPServerSettings
|
|
567
|
+
from fast_agent.mcp.oauth_client import compute_server_identity
|
|
568
|
+
|
|
569
|
+
servers_table = Table(show_header=True, box=None)
|
|
570
|
+
servers_table.add_column("Name", style="white", header_style="bold bright_white")
|
|
571
|
+
servers_table.add_column("Transport", style="white", header_style="bold bright_white")
|
|
572
|
+
servers_table.add_column("Command/URL", header_style="bold bright_white")
|
|
573
|
+
servers_table.add_column("OAuth", header_style="bold bright_white")
|
|
574
|
+
servers_table.add_column("Token", header_style="bold bright_white")
|
|
575
|
+
|
|
576
|
+
for server in mcp_servers:
|
|
577
|
+
name = server["name"]
|
|
578
|
+
transport = server["transport"]
|
|
579
|
+
|
|
580
|
+
# Show either command or URL based on transport type
|
|
581
|
+
if transport == "STDIO":
|
|
582
|
+
command_url = server["command"] or "[dim]Not configured[/dim]"
|
|
583
|
+
else: # SSE
|
|
584
|
+
command_url = server["url"] or "[dim]Not configured[/dim]"
|
|
585
|
+
|
|
586
|
+
# Style configured command/url in green (keep "Not configured" dim)
|
|
587
|
+
if "Not configured" not in command_url:
|
|
588
|
+
command_url = f"[green]{command_url}[/green]"
|
|
589
|
+
|
|
590
|
+
# OAuth status and token presence
|
|
591
|
+
# Default for unsupported transports (e.g., STDIO): show "-" rather than "off"
|
|
592
|
+
oauth_status = "[dim]-[/dim]"
|
|
593
|
+
token_status = "[dim]n/a[/dim]"
|
|
594
|
+
# Attempt to reconstruct minimal server settings for identity check
|
|
595
|
+
try:
|
|
596
|
+
cfg = MCPServerSettings(
|
|
597
|
+
name=name,
|
|
598
|
+
transport="sse"
|
|
599
|
+
if transport == "SSE"
|
|
600
|
+
else ("stdio" if transport == "STDIO" else "http"),
|
|
601
|
+
url=(server.get("url") or None),
|
|
602
|
+
auth=server.get("auth") if isinstance(server.get("auth"), dict) else None,
|
|
603
|
+
)
|
|
604
|
+
except Exception:
|
|
605
|
+
cfg = None
|
|
606
|
+
|
|
607
|
+
if cfg and cfg.transport in ("http", "sse"):
|
|
608
|
+
# Determine if OAuth is enabled for this server
|
|
609
|
+
oauth_enabled = True
|
|
610
|
+
if cfg.auth is not None and hasattr(cfg.auth, "oauth"):
|
|
611
|
+
oauth_enabled = bool(getattr(cfg.auth, "oauth"))
|
|
612
|
+
oauth_status = "[green]on[/green]" if oauth_enabled else "[dim]off[/dim]"
|
|
613
|
+
|
|
614
|
+
# Only check token presence when using keyring persist
|
|
615
|
+
persist = "keyring"
|
|
616
|
+
if cfg.auth is not None and hasattr(cfg.auth, "persist"):
|
|
617
|
+
persist = getattr(cfg.auth, "persist") or "keyring"
|
|
618
|
+
if keyring and keyring_usable and persist == "keyring" and oauth_enabled:
|
|
619
|
+
identity = compute_server_identity(cfg)
|
|
620
|
+
tkey = f"oauth:tokens:{identity}"
|
|
621
|
+
try:
|
|
622
|
+
has = keyring.get_password("fast-agent-mcp", tkey) is not None
|
|
623
|
+
except Exception:
|
|
624
|
+
has = False
|
|
625
|
+
token_status = "[bold green]✓[/bold green]" if has else "[dim]✗[/dim]"
|
|
626
|
+
elif persist == "keyring" and not keyring_usable and oauth_enabled:
|
|
627
|
+
token_status = "[red]not available[/red]"
|
|
628
|
+
elif persist == "memory" and oauth_enabled:
|
|
629
|
+
token_status = "[yellow]memory[/yellow]"
|
|
630
|
+
|
|
631
|
+
servers_table.add_row(name, transport, command_url, oauth_status, token_status)
|
|
632
|
+
|
|
633
|
+
_print_section_header("MCP Servers", color="blue")
|
|
634
|
+
console.print(servers_table)
|
|
635
|
+
|
|
636
|
+
_print_section_header("Agent Skills", color="blue")
|
|
637
|
+
if skills_dir:
|
|
638
|
+
console.print(f"Directory: [green]{_relative_path(skills_dir)}[/green]")
|
|
639
|
+
|
|
640
|
+
if skills_manifests or skill_errors:
|
|
641
|
+
skills_table = Table(show_header=True, box=None)
|
|
642
|
+
skills_table.add_column("Name", style="cyan", header_style="bold bright_white")
|
|
643
|
+
skills_table.add_column("Description", style="white", header_style="bold bright_white")
|
|
644
|
+
skills_table.add_column("Source", style="dim", header_style="bold bright_white")
|
|
645
|
+
skills_table.add_column("Status", style="green", header_style="bold bright_white")
|
|
646
|
+
|
|
647
|
+
def _truncate(text: str, length: int = 70) -> str:
|
|
648
|
+
if len(text) <= length:
|
|
649
|
+
return text
|
|
650
|
+
return text[: length - 3] + "..."
|
|
651
|
+
|
|
652
|
+
for manifest in skills_manifests:
|
|
653
|
+
try:
|
|
654
|
+
relative_source = manifest.path.parent.relative_to(skills_dir)
|
|
655
|
+
source_display = str(relative_source) if relative_source != Path(".") else "."
|
|
656
|
+
except ValueError:
|
|
657
|
+
source_display = _relative_path(manifest.path.parent)
|
|
658
|
+
|
|
659
|
+
skills_table.add_row(
|
|
660
|
+
manifest.name,
|
|
661
|
+
_truncate(manifest.description or ""),
|
|
662
|
+
source_display,
|
|
663
|
+
"[green]ok[/green]",
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
for error in skill_errors:
|
|
667
|
+
error_path_str = error.get("path", "")
|
|
668
|
+
source_display = "[dim]n/a[/dim]"
|
|
669
|
+
if error_path_str:
|
|
670
|
+
error_path = Path(error_path_str)
|
|
671
|
+
try:
|
|
672
|
+
relative_error = error_path.parent.relative_to(skills_dir)
|
|
673
|
+
source_display = str(relative_error) if relative_error != Path(".") else "."
|
|
674
|
+
except ValueError:
|
|
675
|
+
source_display = _relative_path(error_path.parent)
|
|
676
|
+
message = error.get("error", "Failed to parse skill manifest")
|
|
677
|
+
skills_table.add_row(
|
|
678
|
+
"[red]—[/red]",
|
|
679
|
+
"[red]n/a[/red]",
|
|
680
|
+
source_display,
|
|
681
|
+
f"[red]{_truncate(message, 60)}[/red]",
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
console.print(skills_table)
|
|
685
|
+
else:
|
|
686
|
+
console.print("[yellow]No skills found in the directory[/yellow]")
|
|
687
|
+
else:
|
|
688
|
+
if skills_registry.override_failed and override_directory:
|
|
689
|
+
console.print(
|
|
690
|
+
f"[red]Override directory not found:[/red] {_relative_path(override_directory)}"
|
|
691
|
+
)
|
|
692
|
+
console.print(
|
|
693
|
+
"[yellow]Default folders were not loaded because the override failed[/yellow]"
|
|
694
|
+
)
|
|
695
|
+
else:
|
|
696
|
+
console.print(
|
|
697
|
+
"[dim]Agent Skills not configured. Go to https://fast-agent.ai/agents/skills/[/dim]"
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
# Show help tips
|
|
701
|
+
if config_status == "not_found" or secrets_status == "not_found":
|
|
702
|
+
console.print("\n[bold]Setup Tips:[/bold]")
|
|
703
|
+
console.print(
|
|
704
|
+
"Run [cyan]fast-agent setup[/cyan] to create configuration files. Visit [cyan][link=https://fast-agent.ai]fast-agent.ai[/link][/cyan] for configuration guides. "
|
|
705
|
+
)
|
|
706
|
+
elif config_status == "error" or secrets_status == "error":
|
|
707
|
+
console.print("\n[bold]Config File Issues:[/bold]")
|
|
708
|
+
console.print("Fix the YAML syntax errors in your configuration files")
|
|
709
|
+
|
|
710
|
+
if all(
|
|
711
|
+
not api_keys[provider]["env"] and not api_keys[provider]["config"] for provider in api_keys
|
|
712
|
+
):
|
|
713
|
+
console.print(
|
|
714
|
+
"\n[yellow]No API keys configured. Set up API keys to use LLM services:[/yellow]"
|
|
715
|
+
)
|
|
716
|
+
console.print("1. Add keys to fastagent.secrets.yaml")
|
|
717
|
+
env_vars = ", ".join(
|
|
718
|
+
[
|
|
719
|
+
ProviderKeyManager.get_env_key_name(p.value)
|
|
720
|
+
for p in Provider
|
|
721
|
+
if p != Provider.FAST_AGENT
|
|
722
|
+
]
|
|
723
|
+
)
|
|
724
|
+
console.print(f"2. Or set environment variables ({env_vars})")
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
@app.command()
|
|
728
|
+
def show(
|
|
729
|
+
path: str | None = typer.Argument(None, help="Path to configuration file to display"),
|
|
730
|
+
secrets: bool = typer.Option(
|
|
731
|
+
False, "--secrets", "-s", help="Show secrets file instead of config"
|
|
732
|
+
),
|
|
733
|
+
) -> None:
|
|
734
|
+
"""Display the configuration file content or search for it."""
|
|
735
|
+
file_type = "secrets" if secrets else "config"
|
|
736
|
+
|
|
737
|
+
if path:
|
|
738
|
+
config_path = Path(path).resolve()
|
|
739
|
+
if not config_path.exists():
|
|
740
|
+
console.print(
|
|
741
|
+
f"[red]Error:[/red] {file_type.capitalize()} file not found at {config_path}"
|
|
742
|
+
)
|
|
743
|
+
raise typer.Exit(1)
|
|
744
|
+
else:
|
|
745
|
+
config_files = find_config_files(Path.cwd())
|
|
746
|
+
config_path = config_files[file_type]
|
|
747
|
+
if not config_path:
|
|
748
|
+
console.print(
|
|
749
|
+
f"[yellow]No {file_type} file found in current directory or parents[/yellow]"
|
|
750
|
+
)
|
|
751
|
+
console.print("Run [cyan]fast-agent setup[/cyan] to create configuration files")
|
|
752
|
+
raise typer.Exit(1)
|
|
753
|
+
|
|
754
|
+
console.print(f"\n[bold]{file_type.capitalize()} file:[/bold] {config_path}\n")
|
|
755
|
+
|
|
756
|
+
try:
|
|
757
|
+
with open(config_path, "r") as f:
|
|
758
|
+
content = f.read()
|
|
759
|
+
|
|
760
|
+
# Try to parse as YAML to check validity
|
|
761
|
+
parsed = yaml.safe_load(content)
|
|
762
|
+
|
|
763
|
+
# Show parsing success status
|
|
764
|
+
console.print("[green]YAML syntax is valid[/green]")
|
|
765
|
+
if parsed is None:
|
|
766
|
+
console.print("[yellow]Warning: File is empty or contains only comments[/yellow]\n")
|
|
767
|
+
else:
|
|
768
|
+
console.print(
|
|
769
|
+
f"[green]Successfully parsed {len(parsed) if isinstance(parsed, dict) else 0} root keys[/green]\n"
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
# Print the content
|
|
773
|
+
console.print(content)
|
|
774
|
+
|
|
775
|
+
except Exception as e:
|
|
776
|
+
console.print(f"[red]Error parsing {file_type} file:[/red] {e}")
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
@app.callback(invoke_without_command=True)
|
|
780
|
+
def main(ctx: typer.Context) -> None:
|
|
781
|
+
"""Check and diagnose FastAgent configuration."""
|
|
782
|
+
if ctx.invoked_subcommand is None:
|
|
783
|
+
show_check_summary()
|