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,857 @@
|
|
|
1
|
+
"""Rendering helpers for MCP status information in the enhanced prompt UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import TYPE_CHECKING, Iterable
|
|
7
|
+
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from fast_agent.ui import console
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from fast_agent.mcp.mcp_aggregator import ServerStatus
|
|
14
|
+
from fast_agent.mcp.transport_tracking import ChannelSnapshot
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Centralized color configuration
|
|
18
|
+
class Colours:
|
|
19
|
+
"""Color constants for MCP status display elements."""
|
|
20
|
+
|
|
21
|
+
# Timeline activity colors (Option A: Mixed Intensity)
|
|
22
|
+
ERROR = "bright_red" # Keep error bright
|
|
23
|
+
DISABLED = "bright_blue" # Keep disabled bright
|
|
24
|
+
RESPONSE = "blue" # Normal blue instead of bright
|
|
25
|
+
REQUEST = "yellow" # Normal yellow instead of bright
|
|
26
|
+
NOTIFICATION = "cyan" # Normal cyan instead of bright
|
|
27
|
+
PING = "dim green" # Keep ping dim
|
|
28
|
+
IDLE = "white dim"
|
|
29
|
+
NONE = "dim"
|
|
30
|
+
|
|
31
|
+
# Channel arrow states
|
|
32
|
+
ARROW_ERROR = "bright_red"
|
|
33
|
+
ARROW_DISABLED = "bright_yellow" # For explicitly disabled/off
|
|
34
|
+
ARROW_METHOD_NOT_ALLOWED = "cyan" # For 405 method not allowed (notification color)
|
|
35
|
+
ARROW_OFF = "black dim"
|
|
36
|
+
ARROW_IDLE = "bright_cyan" # Connected but no activity
|
|
37
|
+
ARROW_ACTIVE = "bright_green" # Connected with activity
|
|
38
|
+
|
|
39
|
+
# Capability token states
|
|
40
|
+
TOKEN_ERROR = "bright_red"
|
|
41
|
+
TOKEN_WARNING = "bright_cyan"
|
|
42
|
+
TOKEN_CAUTION = "bright_yellow"
|
|
43
|
+
TOKEN_DISABLED = "dim"
|
|
44
|
+
TOKEN_HIGHLIGHTED = "bright_yellow"
|
|
45
|
+
TOKEN_ENABLED = "bright_green"
|
|
46
|
+
|
|
47
|
+
# Text elements
|
|
48
|
+
TEXT_DIM = "dim"
|
|
49
|
+
TEXT_DEFAULT = "default" # Use terminal's default text color
|
|
50
|
+
TEXT_BRIGHT = "bright_white"
|
|
51
|
+
TEXT_ERROR = "bright_red"
|
|
52
|
+
TEXT_WARNING = "bright_yellow"
|
|
53
|
+
TEXT_SUCCESS = "bright_green"
|
|
54
|
+
TEXT_INFO = "bright_blue"
|
|
55
|
+
TEXT_CYAN = "cyan"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Symbol definitions for timelines and legends
|
|
59
|
+
SYMBOL_IDLE = "·"
|
|
60
|
+
SYMBOL_ERROR = "●"
|
|
61
|
+
SYMBOL_RESPONSE = "▼"
|
|
62
|
+
SYMBOL_NOTIFICATION = "●"
|
|
63
|
+
SYMBOL_REQUEST = "◆"
|
|
64
|
+
SYMBOL_STDIO_ACTIVITY = "●"
|
|
65
|
+
SYMBOL_PING = "●"
|
|
66
|
+
SYMBOL_DISABLED = "▽"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Color mappings for different contexts
|
|
70
|
+
TIMELINE_COLORS = {
|
|
71
|
+
"error": Colours.ERROR,
|
|
72
|
+
"disabled": Colours.DISABLED,
|
|
73
|
+
"response": Colours.RESPONSE,
|
|
74
|
+
"request": Colours.REQUEST,
|
|
75
|
+
"notification": Colours.NOTIFICATION,
|
|
76
|
+
"ping": Colours.PING,
|
|
77
|
+
"none": Colours.IDLE,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
TIMELINE_COLORS_STDIO = {
|
|
81
|
+
"error": Colours.ERROR,
|
|
82
|
+
"request": Colours.TOKEN_ENABLED, # All activity shows as bright green
|
|
83
|
+
"response": Colours.TOKEN_ENABLED,
|
|
84
|
+
"notification": Colours.TOKEN_ENABLED,
|
|
85
|
+
"ping": Colours.PING,
|
|
86
|
+
"none": Colours.IDLE,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _format_compact_duration(seconds: float | None) -> str | None:
|
|
91
|
+
if seconds is None:
|
|
92
|
+
return None
|
|
93
|
+
total = int(seconds)
|
|
94
|
+
if total < 1:
|
|
95
|
+
return "<1s"
|
|
96
|
+
mins, secs = divmod(total, 60)
|
|
97
|
+
if mins == 0:
|
|
98
|
+
return f"{secs}s"
|
|
99
|
+
hours, mins = divmod(mins, 60)
|
|
100
|
+
if hours == 0:
|
|
101
|
+
return f"{mins}m{secs:02d}s"
|
|
102
|
+
days, hours = divmod(hours, 24)
|
|
103
|
+
if days == 0:
|
|
104
|
+
return f"{hours}h{mins:02d}m"
|
|
105
|
+
return f"{days}d{hours:02d}h"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _format_timeline_label(total_seconds: int) -> str:
|
|
109
|
+
total = max(0, int(total_seconds))
|
|
110
|
+
if total == 0:
|
|
111
|
+
return "0s"
|
|
112
|
+
|
|
113
|
+
days, remainder = divmod(total, 86400)
|
|
114
|
+
if days:
|
|
115
|
+
if remainder == 0:
|
|
116
|
+
return f"{days}d"
|
|
117
|
+
hours = remainder // 3600
|
|
118
|
+
if hours == 0:
|
|
119
|
+
return f"{days}d"
|
|
120
|
+
return f"{days}d{hours}h"
|
|
121
|
+
|
|
122
|
+
hours, remainder = divmod(total, 3600)
|
|
123
|
+
if hours:
|
|
124
|
+
if remainder == 0:
|
|
125
|
+
return f"{hours}h"
|
|
126
|
+
minutes = remainder // 60
|
|
127
|
+
if minutes == 0:
|
|
128
|
+
return f"{hours}h"
|
|
129
|
+
return f"{hours}h{minutes:02d}m"
|
|
130
|
+
|
|
131
|
+
minutes, seconds = divmod(total, 60)
|
|
132
|
+
if minutes:
|
|
133
|
+
if seconds == 0:
|
|
134
|
+
return f"{minutes}m"
|
|
135
|
+
return f"{minutes}m{seconds:02d}s"
|
|
136
|
+
|
|
137
|
+
return f"{seconds}s"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _summarise_call_counts(call_counts: dict[str, int]) -> str | None:
|
|
141
|
+
if not call_counts:
|
|
142
|
+
return None
|
|
143
|
+
ordered = sorted(call_counts.items(), key=lambda item: item[0])
|
|
144
|
+
return ", ".join(f"{name}:{count}" for name, count in ordered)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _format_session_id(session_id: str | None) -> Text:
|
|
148
|
+
text = Text()
|
|
149
|
+
if not session_id:
|
|
150
|
+
text.append("None", style="yellow")
|
|
151
|
+
return text
|
|
152
|
+
if session_id == "local":
|
|
153
|
+
text.append("local", style="cyan")
|
|
154
|
+
return text
|
|
155
|
+
|
|
156
|
+
# Only trim if excessively long (>24 chars)
|
|
157
|
+
value = session_id
|
|
158
|
+
if len(session_id) > 24:
|
|
159
|
+
# Trim middle to preserve start and end
|
|
160
|
+
value = f"{session_id[:10]}...{session_id[-10:]}"
|
|
161
|
+
text.append(value, style="green")
|
|
162
|
+
return text
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _build_aligned_field(
|
|
166
|
+
label: str, value: Text | str, *, label_width: int = 9, value_style: str = Colours.TEXT_DEFAULT
|
|
167
|
+
) -> Text:
|
|
168
|
+
field = Text()
|
|
169
|
+
field.append(f"{label:<{label_width}}: ", style="dim")
|
|
170
|
+
if isinstance(value, Text):
|
|
171
|
+
field.append_text(value)
|
|
172
|
+
else:
|
|
173
|
+
field.append(value, style=value_style)
|
|
174
|
+
return field
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _cap_attr(source, attr: str | None) -> bool:
|
|
178
|
+
if source is None:
|
|
179
|
+
return False
|
|
180
|
+
target = source
|
|
181
|
+
if attr:
|
|
182
|
+
if isinstance(source, dict):
|
|
183
|
+
target = source.get(attr)
|
|
184
|
+
else:
|
|
185
|
+
target = getattr(source, attr, None)
|
|
186
|
+
if isinstance(target, bool):
|
|
187
|
+
return target
|
|
188
|
+
return bool(target)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _format_capability_shorthand(
|
|
192
|
+
status: ServerStatus, template_expected: bool
|
|
193
|
+
) -> tuple[list[tuple[str, str]], list[tuple[str, str]]]:
|
|
194
|
+
caps = status.server_capabilities
|
|
195
|
+
tools = getattr(caps, "tools", None)
|
|
196
|
+
prompts = getattr(caps, "prompts", None)
|
|
197
|
+
resources = getattr(caps, "resources", None)
|
|
198
|
+
logging_caps = getattr(caps, "logging", None)
|
|
199
|
+
completion_caps = (
|
|
200
|
+
getattr(caps, "completion", None)
|
|
201
|
+
or getattr(caps, "completions", None)
|
|
202
|
+
or getattr(caps, "respond", None)
|
|
203
|
+
)
|
|
204
|
+
experimental_caps = getattr(caps, "experimental", None)
|
|
205
|
+
|
|
206
|
+
instructions_available = bool(status.instructions_available)
|
|
207
|
+
instructions_enabled = status.instructions_enabled
|
|
208
|
+
|
|
209
|
+
entries = [
|
|
210
|
+
("To", _cap_attr(tools, None), _cap_attr(tools, "listChanged")),
|
|
211
|
+
("Pr", _cap_attr(prompts, None), _cap_attr(prompts, "listChanged")),
|
|
212
|
+
(
|
|
213
|
+
"Re",
|
|
214
|
+
_cap_attr(resources, "read") or _cap_attr(resources, None),
|
|
215
|
+
_cap_attr(resources, "listChanged"),
|
|
216
|
+
),
|
|
217
|
+
("Rs", _cap_attr(resources, "subscribe"), _cap_attr(resources, "subscribe")),
|
|
218
|
+
("Lo", _cap_attr(logging_caps, None), False),
|
|
219
|
+
("Co", _cap_attr(completion_caps, None), _cap_attr(completion_caps, "listChanged")),
|
|
220
|
+
("Ex", _cap_attr(experimental_caps, None), False),
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
if not instructions_available:
|
|
224
|
+
entries.append(("In", False, False))
|
|
225
|
+
elif instructions_enabled is False:
|
|
226
|
+
entries.append(("In", "red", False))
|
|
227
|
+
elif instructions_enabled is None and not template_expected:
|
|
228
|
+
entries.append(("In", "warn", False))
|
|
229
|
+
elif instructions_enabled is None:
|
|
230
|
+
entries.append(("In", True, False))
|
|
231
|
+
elif template_expected:
|
|
232
|
+
entries.append(("In", True, False))
|
|
233
|
+
else:
|
|
234
|
+
entries.append(("In", "blue", False))
|
|
235
|
+
|
|
236
|
+
skybridge_config = getattr(status, "skybridge", None)
|
|
237
|
+
if not skybridge_config:
|
|
238
|
+
entries.append(("Sk", False, False))
|
|
239
|
+
else:
|
|
240
|
+
has_warnings = bool(getattr(skybridge_config, "warnings", None))
|
|
241
|
+
if has_warnings:
|
|
242
|
+
entries.append(("Sk", "warn", False))
|
|
243
|
+
elif getattr(skybridge_config, "enabled", False):
|
|
244
|
+
entries.append(("Sk", True, False))
|
|
245
|
+
else:
|
|
246
|
+
entries.append(("Sk", False, False))
|
|
247
|
+
|
|
248
|
+
if status.roots_configured:
|
|
249
|
+
entries.append(("Ro", True, False))
|
|
250
|
+
else:
|
|
251
|
+
entries.append(("Ro", False, False))
|
|
252
|
+
|
|
253
|
+
mode = (status.elicitation_mode or "").lower()
|
|
254
|
+
if mode == "auto-cancel":
|
|
255
|
+
entries.append(("El", "red", False))
|
|
256
|
+
elif mode and mode != "none":
|
|
257
|
+
entries.append(("El", True, False))
|
|
258
|
+
else:
|
|
259
|
+
entries.append(("El", False, False))
|
|
260
|
+
|
|
261
|
+
sampling_mode = (status.sampling_mode or "").lower()
|
|
262
|
+
if sampling_mode == "configured":
|
|
263
|
+
entries.append(("Sa", "blue", False))
|
|
264
|
+
elif sampling_mode == "auto":
|
|
265
|
+
entries.append(("Sa", True, False))
|
|
266
|
+
else:
|
|
267
|
+
entries.append(("Sa", False, False))
|
|
268
|
+
|
|
269
|
+
entries.append(("Sp", bool(status.spoofing_enabled), False))
|
|
270
|
+
|
|
271
|
+
def token_style(supported, highlighted) -> str:
|
|
272
|
+
if supported == "red":
|
|
273
|
+
return Colours.TOKEN_ERROR
|
|
274
|
+
if supported == "blue":
|
|
275
|
+
return Colours.TOKEN_WARNING
|
|
276
|
+
if supported == "warn":
|
|
277
|
+
return Colours.TOKEN_CAUTION
|
|
278
|
+
if not supported:
|
|
279
|
+
return Colours.TOKEN_DISABLED
|
|
280
|
+
if highlighted:
|
|
281
|
+
return Colours.TOKEN_HIGHLIGHTED
|
|
282
|
+
return Colours.TOKEN_ENABLED
|
|
283
|
+
|
|
284
|
+
tokens = [
|
|
285
|
+
(label, token_style(supported, highlighted)) for label, supported, highlighted in entries
|
|
286
|
+
]
|
|
287
|
+
return tokens[:8], tokens[8:]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _build_capability_text(tokens: list[tuple[str, str]]) -> Text:
|
|
291
|
+
line = Text()
|
|
292
|
+
host_boundary_inserted = False
|
|
293
|
+
for idx, (label, style) in enumerate(tokens):
|
|
294
|
+
if idx:
|
|
295
|
+
line.append(" ")
|
|
296
|
+
if not host_boundary_inserted and label == "Ro":
|
|
297
|
+
line.append("• ", style="dim")
|
|
298
|
+
host_boundary_inserted = True
|
|
299
|
+
line.append(label, style=style)
|
|
300
|
+
return line
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _format_relative_time(dt: datetime | None) -> str:
|
|
304
|
+
if dt is None:
|
|
305
|
+
return "never"
|
|
306
|
+
try:
|
|
307
|
+
now = datetime.now(timezone.utc)
|
|
308
|
+
except Exception:
|
|
309
|
+
now = datetime.utcnow().replace(tzinfo=timezone.utc)
|
|
310
|
+
seconds = max(0, (now - dt).total_seconds())
|
|
311
|
+
return _format_compact_duration(seconds) or "<1s"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _format_label(label: str, width: int = 10) -> str:
|
|
315
|
+
return f"{label:<{width}}" if len(label) < width else label
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _build_inline_timeline(
|
|
319
|
+
buckets: Iterable[str],
|
|
320
|
+
*,
|
|
321
|
+
bucket_seconds: int | None = None,
|
|
322
|
+
bucket_count: int | None = None,
|
|
323
|
+
) -> str:
|
|
324
|
+
"""Build a compact timeline string for inline display."""
|
|
325
|
+
bucket_list = list(buckets)
|
|
326
|
+
count = bucket_count or len(bucket_list)
|
|
327
|
+
if count <= 0:
|
|
328
|
+
count = len(bucket_list) or 1
|
|
329
|
+
|
|
330
|
+
seconds = bucket_seconds or 30
|
|
331
|
+
total_window = seconds * count
|
|
332
|
+
timeline = f" [dim]{_format_timeline_label(total_window)}[/dim] "
|
|
333
|
+
|
|
334
|
+
if len(bucket_list) < count:
|
|
335
|
+
bucket_list.extend(["none"] * (count - len(bucket_list)))
|
|
336
|
+
elif len(bucket_list) > count:
|
|
337
|
+
bucket_list = bucket_list[-count:]
|
|
338
|
+
|
|
339
|
+
for state in bucket_list:
|
|
340
|
+
color = TIMELINE_COLORS.get(state, Colours.NONE)
|
|
341
|
+
if state in {"idle", "none"}:
|
|
342
|
+
symbol = SYMBOL_IDLE
|
|
343
|
+
elif state == "request":
|
|
344
|
+
symbol = SYMBOL_REQUEST
|
|
345
|
+
elif state == "notification":
|
|
346
|
+
symbol = SYMBOL_NOTIFICATION
|
|
347
|
+
elif state == "error":
|
|
348
|
+
symbol = SYMBOL_ERROR
|
|
349
|
+
elif state == "ping":
|
|
350
|
+
symbol = SYMBOL_PING
|
|
351
|
+
elif state == "disabled":
|
|
352
|
+
symbol = SYMBOL_DISABLED
|
|
353
|
+
else:
|
|
354
|
+
symbol = SYMBOL_RESPONSE
|
|
355
|
+
timeline += f"[bold {color}]{symbol}[/bold {color}]"
|
|
356
|
+
timeline += " [dim]now[/dim]"
|
|
357
|
+
return timeline
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _render_channel_summary(status: ServerStatus, indent: str, total_width: int) -> None:
|
|
361
|
+
snapshot = getattr(status, "transport_channels", None)
|
|
362
|
+
if snapshot is None:
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
transport_value = getattr(status, "transport", None)
|
|
366
|
+
transport_lower = (transport_value or "").lower()
|
|
367
|
+
is_sse_transport = transport_lower == "sse"
|
|
368
|
+
|
|
369
|
+
# Show channel types based on what's available
|
|
370
|
+
entries: list[tuple[str, str, ChannelSnapshot | None]] = []
|
|
371
|
+
|
|
372
|
+
# Check if we have HTTP transport channels
|
|
373
|
+
http_channels = [
|
|
374
|
+
getattr(snapshot, "get", None),
|
|
375
|
+
getattr(snapshot, "post_sse", None),
|
|
376
|
+
getattr(snapshot, "post_json", None),
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
# Check if we have stdio transport channel
|
|
380
|
+
stdio_channel = getattr(snapshot, "stdio", None)
|
|
381
|
+
|
|
382
|
+
if any(channel is not None for channel in http_channels):
|
|
383
|
+
# HTTP or SSE transport - show available channels
|
|
384
|
+
entries = [
|
|
385
|
+
("GET (SSE)", "◀", getattr(snapshot, "get", None)),
|
|
386
|
+
("POST (SSE)", "▶", getattr(snapshot, "post_sse", None)),
|
|
387
|
+
]
|
|
388
|
+
if not is_sse_transport:
|
|
389
|
+
entries.append(("POST (JSON)", "▶", getattr(snapshot, "post_json", None)))
|
|
390
|
+
elif stdio_channel is not None:
|
|
391
|
+
# STDIO transport - show single bidirectional channel
|
|
392
|
+
entries = [
|
|
393
|
+
("STDIO", "⇄", stdio_channel),
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
# Skip if no channels have data
|
|
397
|
+
if not any(channel is not None for _, _, channel in entries):
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
console.console.print() # Add space before channels
|
|
401
|
+
|
|
402
|
+
# Determine if we're showing stdio or HTTP channels
|
|
403
|
+
is_stdio = stdio_channel is not None
|
|
404
|
+
|
|
405
|
+
default_bucket_seconds = getattr(snapshot, "activity_bucket_seconds", None) or 30
|
|
406
|
+
default_bucket_count = getattr(snapshot, "activity_bucket_count", None) or 20
|
|
407
|
+
timeline_header_label = _format_timeline_label(default_bucket_seconds * default_bucket_count)
|
|
408
|
+
|
|
409
|
+
# Total characters before the metrics section in each row (excluding indent)
|
|
410
|
+
# Structure: "│ " + arrow + " " + label(13) + timeline_label + " " + buckets + " now"
|
|
411
|
+
metrics_prefix_width = 22 + len(timeline_header_label) + default_bucket_count
|
|
412
|
+
|
|
413
|
+
# Get transport type for display
|
|
414
|
+
transport = transport_value or "unknown"
|
|
415
|
+
transport_display = transport.upper() if transport != "unknown" else "Channels"
|
|
416
|
+
|
|
417
|
+
# Header with column labels
|
|
418
|
+
header = Text(indent)
|
|
419
|
+
header_intro = f"┌ {transport_display} "
|
|
420
|
+
header.append(header_intro, style="dim")
|
|
421
|
+
|
|
422
|
+
# Calculate padding needed based on transport display length
|
|
423
|
+
header_prefix_len = len(header_intro)
|
|
424
|
+
|
|
425
|
+
dash_count = max(1, metrics_prefix_width - header_prefix_len + 2)
|
|
426
|
+
if is_stdio:
|
|
427
|
+
header.append("─" * dash_count, style="dim")
|
|
428
|
+
header.append(" activity", style="dim")
|
|
429
|
+
else:
|
|
430
|
+
header.append("─" * dash_count, style="dim")
|
|
431
|
+
header.append(" req resp notif ping", style="dim")
|
|
432
|
+
|
|
433
|
+
console.console.print(header)
|
|
434
|
+
|
|
435
|
+
# Empty row after header for cleaner spacing
|
|
436
|
+
empty_header = Text(indent)
|
|
437
|
+
empty_header.append("│", style="dim")
|
|
438
|
+
console.console.print(empty_header)
|
|
439
|
+
|
|
440
|
+
# Collect any errors to show at bottom
|
|
441
|
+
errors = []
|
|
442
|
+
|
|
443
|
+
# Get appropriate timeline color map
|
|
444
|
+
timeline_color_map = TIMELINE_COLORS_STDIO if is_stdio else TIMELINE_COLORS
|
|
445
|
+
|
|
446
|
+
for label, arrow, channel in entries:
|
|
447
|
+
line = Text(indent)
|
|
448
|
+
line.append("│ ", style="dim")
|
|
449
|
+
|
|
450
|
+
# Determine arrow color based on state
|
|
451
|
+
arrow_style = Colours.ARROW_OFF # default no channel
|
|
452
|
+
if channel:
|
|
453
|
+
state = (channel.state or "open").lower()
|
|
454
|
+
|
|
455
|
+
# Check for 405 status code (method not allowed = not an error, just unsupported)
|
|
456
|
+
if channel.last_status_code == 405:
|
|
457
|
+
arrow_style = Colours.ARROW_METHOD_NOT_ALLOWED
|
|
458
|
+
# Don't add 405 to errors list - it's not an error, just method not supported
|
|
459
|
+
# Error state (non-405 errors)
|
|
460
|
+
elif state == "error":
|
|
461
|
+
arrow_style = Colours.ARROW_ERROR
|
|
462
|
+
if channel.last_error and channel.last_status_code != 405:
|
|
463
|
+
error_msg = channel.last_error
|
|
464
|
+
if channel.last_status_code:
|
|
465
|
+
errors.append(
|
|
466
|
+
(label.split()[0], f"{error_msg} ({channel.last_status_code})")
|
|
467
|
+
)
|
|
468
|
+
else:
|
|
469
|
+
errors.append((label.split()[0], error_msg))
|
|
470
|
+
# Explicitly disabled or off
|
|
471
|
+
elif state in {"off", "disabled"}:
|
|
472
|
+
arrow_style = Colours.ARROW_OFF
|
|
473
|
+
# No activity (idle)
|
|
474
|
+
elif channel.request_count == 0 and channel.response_count == 0:
|
|
475
|
+
arrow_style = Colours.ARROW_IDLE
|
|
476
|
+
# Active/connected with activity
|
|
477
|
+
elif state in {"open", "connected"}:
|
|
478
|
+
arrow_style = Colours.ARROW_ACTIVE
|
|
479
|
+
# Fallback for other states
|
|
480
|
+
else:
|
|
481
|
+
arrow_style = Colours.ARROW_IDLE
|
|
482
|
+
|
|
483
|
+
# Arrow and label with better spacing
|
|
484
|
+
# Use hollow arrow for 405 Method Not Allowed
|
|
485
|
+
if channel and channel.last_status_code == 405:
|
|
486
|
+
# Convert solid arrows to hollow for 405
|
|
487
|
+
hollow_arrows = {"◀": "◁", "▶": "▷", "⇄": "⇄"} # bidirectional stays same
|
|
488
|
+
display_arrow = hollow_arrows.get(arrow, arrow)
|
|
489
|
+
else:
|
|
490
|
+
display_arrow = arrow
|
|
491
|
+
line.append(display_arrow, style=arrow_style)
|
|
492
|
+
|
|
493
|
+
# Determine label style based on activity and special cases
|
|
494
|
+
if not channel:
|
|
495
|
+
# No channel = dim
|
|
496
|
+
label_style = Colours.TEXT_DIM
|
|
497
|
+
elif channel.last_status_code == 405 and "GET" in label:
|
|
498
|
+
# Special case: GET (SSE) with 405 = dim (hollow arrow already handled above)
|
|
499
|
+
label_style = Colours.TEXT_DIM
|
|
500
|
+
elif arrow_style == Colours.ARROW_ERROR and "GET" in label:
|
|
501
|
+
# Highlight GET stream errors in red to match the arrow indicator
|
|
502
|
+
label_style = Colours.TEXT_ERROR
|
|
503
|
+
elif (
|
|
504
|
+
channel.request_count == 0
|
|
505
|
+
and channel.response_count == 0
|
|
506
|
+
and channel.notification_count == 0
|
|
507
|
+
and (channel.ping_count or 0) == 0
|
|
508
|
+
):
|
|
509
|
+
# No activity = dim
|
|
510
|
+
label_style = Colours.TEXT_DIM
|
|
511
|
+
else:
|
|
512
|
+
# Has activity = normal
|
|
513
|
+
label_style = Colours.TEXT_DEFAULT
|
|
514
|
+
line.append(f" {label:<13}", style=label_style)
|
|
515
|
+
|
|
516
|
+
# Always show timeline (dim black dots if no data)
|
|
517
|
+
channel_bucket_seconds = (
|
|
518
|
+
getattr(channel, "activity_bucket_seconds", None) or default_bucket_seconds
|
|
519
|
+
)
|
|
520
|
+
bucket_count = (
|
|
521
|
+
len(channel.activity_buckets)
|
|
522
|
+
if channel and channel.activity_buckets
|
|
523
|
+
else getattr(channel, "activity_bucket_count", None)
|
|
524
|
+
)
|
|
525
|
+
if not bucket_count or bucket_count <= 0:
|
|
526
|
+
bucket_count = default_bucket_count
|
|
527
|
+
total_window_seconds = channel_bucket_seconds * bucket_count
|
|
528
|
+
timeline_label = _format_timeline_label(total_window_seconds)
|
|
529
|
+
|
|
530
|
+
line.append(f"{timeline_label} ", style="dim")
|
|
531
|
+
bucket_states = channel.activity_buckets if channel and channel.activity_buckets else None
|
|
532
|
+
if bucket_states:
|
|
533
|
+
# Show actual activity
|
|
534
|
+
for bucket_state in bucket_states:
|
|
535
|
+
color = timeline_color_map.get(bucket_state, "dim")
|
|
536
|
+
if bucket_state in {"idle", "none"}:
|
|
537
|
+
symbol = SYMBOL_IDLE
|
|
538
|
+
elif is_stdio:
|
|
539
|
+
symbol = SYMBOL_STDIO_ACTIVITY
|
|
540
|
+
elif bucket_state == "request":
|
|
541
|
+
symbol = SYMBOL_REQUEST
|
|
542
|
+
elif bucket_state == "notification":
|
|
543
|
+
symbol = SYMBOL_NOTIFICATION
|
|
544
|
+
elif bucket_state == "error":
|
|
545
|
+
symbol = SYMBOL_ERROR
|
|
546
|
+
elif bucket_state == "ping":
|
|
547
|
+
symbol = SYMBOL_PING
|
|
548
|
+
elif bucket_state == "disabled":
|
|
549
|
+
symbol = SYMBOL_DISABLED
|
|
550
|
+
else:
|
|
551
|
+
symbol = SYMBOL_RESPONSE
|
|
552
|
+
line.append(symbol, style=f"bold {color}")
|
|
553
|
+
else:
|
|
554
|
+
# Show dim dots for no activity
|
|
555
|
+
for _ in range(bucket_count):
|
|
556
|
+
line.append(SYMBOL_IDLE, style="black dim")
|
|
557
|
+
line.append(" now", style="dim")
|
|
558
|
+
|
|
559
|
+
# Metrics - different layouts for stdio vs HTTP
|
|
560
|
+
if is_stdio:
|
|
561
|
+
# Simplified activity column for stdio
|
|
562
|
+
if channel and channel.message_count > 0:
|
|
563
|
+
activity = str(channel.message_count).rjust(8)
|
|
564
|
+
activity_style = Colours.TEXT_DEFAULT
|
|
565
|
+
else:
|
|
566
|
+
activity = "-".rjust(8)
|
|
567
|
+
activity_style = Colours.TEXT_DIM
|
|
568
|
+
line.append(f" {activity}", style=activity_style)
|
|
569
|
+
else:
|
|
570
|
+
# Original HTTP columns
|
|
571
|
+
if channel:
|
|
572
|
+
# Show "-" for shut/disabled channels (405, off, disabled states)
|
|
573
|
+
channel_state = (channel.state or "open").lower()
|
|
574
|
+
is_shut = (
|
|
575
|
+
channel.last_status_code == 405
|
|
576
|
+
or channel_state in {"off", "disabled"}
|
|
577
|
+
or (channel_state == "error" and channel.last_status_code == 405)
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
if is_shut:
|
|
581
|
+
req = "-".rjust(5)
|
|
582
|
+
resp = "-".rjust(5)
|
|
583
|
+
notif = "-".rjust(5)
|
|
584
|
+
ping = "-".rjust(5)
|
|
585
|
+
metrics_style = Colours.TEXT_DIM
|
|
586
|
+
else:
|
|
587
|
+
req = str(channel.request_count).rjust(5)
|
|
588
|
+
resp = str(channel.response_count).rjust(5)
|
|
589
|
+
notif = str(channel.notification_count).rjust(5)
|
|
590
|
+
ping = str(channel.ping_count if channel.ping_count else "-").rjust(5)
|
|
591
|
+
metrics_style = Colours.TEXT_DEFAULT
|
|
592
|
+
else:
|
|
593
|
+
req = "-".rjust(5)
|
|
594
|
+
resp = "-".rjust(5)
|
|
595
|
+
notif = "-".rjust(5)
|
|
596
|
+
ping = "-".rjust(5)
|
|
597
|
+
metrics_style = Colours.TEXT_DIM
|
|
598
|
+
line.append(f" {req} {resp} {notif} {ping}", style=metrics_style)
|
|
599
|
+
|
|
600
|
+
console.console.print(line)
|
|
601
|
+
|
|
602
|
+
# Debug: print the raw line length
|
|
603
|
+
# import sys
|
|
604
|
+
# print(f"Line length: {len(line.plain)}", file=sys.stderr)
|
|
605
|
+
|
|
606
|
+
# Show errors at bottom if any
|
|
607
|
+
if errors:
|
|
608
|
+
# Empty row before errors
|
|
609
|
+
empty_line = Text(indent)
|
|
610
|
+
empty_line.append("│", style="dim")
|
|
611
|
+
console.console.print(empty_line)
|
|
612
|
+
|
|
613
|
+
for channel_type, error_msg in errors:
|
|
614
|
+
error_line = Text(indent)
|
|
615
|
+
error_line.append("│ ", style=Colours.TEXT_DIM)
|
|
616
|
+
error_line.append("⚠ ", style=Colours.TEXT_WARNING)
|
|
617
|
+
error_line.append(f"{channel_type}: ", style=Colours.TEXT_DEFAULT)
|
|
618
|
+
# Truncate long error messages
|
|
619
|
+
if len(error_msg) > 60:
|
|
620
|
+
error_msg = error_msg[:57] + "..."
|
|
621
|
+
error_line.append(error_msg, style=Colours.TEXT_ERROR)
|
|
622
|
+
console.console.print(error_line)
|
|
623
|
+
|
|
624
|
+
# Legend if any timelines shown
|
|
625
|
+
has_timelines = any(channel and channel.activity_buckets for _, _, channel in entries)
|
|
626
|
+
|
|
627
|
+
if has_timelines:
|
|
628
|
+
# Empty row before footer with legend
|
|
629
|
+
empty_before = Text(indent)
|
|
630
|
+
empty_before.append("│", style="dim")
|
|
631
|
+
console.console.print(empty_before)
|
|
632
|
+
|
|
633
|
+
# Footer with legend
|
|
634
|
+
footer = Text(indent)
|
|
635
|
+
footer.append("└", style="dim")
|
|
636
|
+
|
|
637
|
+
if has_timelines:
|
|
638
|
+
footer.append(" legend: ", style="dim")
|
|
639
|
+
|
|
640
|
+
if is_stdio:
|
|
641
|
+
# Simplified legend for stdio: just activity vs idle
|
|
642
|
+
legend_map = [
|
|
643
|
+
("activity", f"bold {Colours.TOKEN_ENABLED}"),
|
|
644
|
+
("idle", Colours.IDLE),
|
|
645
|
+
]
|
|
646
|
+
else:
|
|
647
|
+
# Full legend for HTTP channels
|
|
648
|
+
legend_map = [
|
|
649
|
+
("error", f"bold {Colours.ERROR}"),
|
|
650
|
+
("response", f"bold {Colours.RESPONSE}"),
|
|
651
|
+
("request", f"bold {Colours.REQUEST}"),
|
|
652
|
+
("notification", f"bold {Colours.NOTIFICATION}"),
|
|
653
|
+
("ping", Colours.PING),
|
|
654
|
+
("idle", Colours.IDLE),
|
|
655
|
+
]
|
|
656
|
+
|
|
657
|
+
for i, (name, color) in enumerate(legend_map):
|
|
658
|
+
if i > 0:
|
|
659
|
+
footer.append(" ", style="dim")
|
|
660
|
+
if name == "idle":
|
|
661
|
+
symbol = SYMBOL_IDLE
|
|
662
|
+
elif name == "request":
|
|
663
|
+
symbol = SYMBOL_REQUEST
|
|
664
|
+
elif name == "notification":
|
|
665
|
+
symbol = SYMBOL_NOTIFICATION
|
|
666
|
+
elif name == "error":
|
|
667
|
+
symbol = SYMBOL_ERROR
|
|
668
|
+
elif name == "ping":
|
|
669
|
+
symbol = SYMBOL_PING
|
|
670
|
+
elif is_stdio and name == "activity":
|
|
671
|
+
symbol = SYMBOL_STDIO_ACTIVITY
|
|
672
|
+
else:
|
|
673
|
+
symbol = SYMBOL_RESPONSE
|
|
674
|
+
footer.append(symbol, style=f"{color}")
|
|
675
|
+
footer.append(f" {name}", style="dim")
|
|
676
|
+
|
|
677
|
+
console.console.print(footer)
|
|
678
|
+
|
|
679
|
+
# Add blank line for spacing before capabilities
|
|
680
|
+
console.console.print()
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
async def render_mcp_status(agent, indent: str = "") -> None:
|
|
684
|
+
server_status_map = {}
|
|
685
|
+
if hasattr(agent, "get_server_status") and callable(getattr(agent, "get_server_status")):
|
|
686
|
+
try:
|
|
687
|
+
server_status_map = await agent.get_server_status()
|
|
688
|
+
except Exception:
|
|
689
|
+
server_status_map = {}
|
|
690
|
+
|
|
691
|
+
if not server_status_map:
|
|
692
|
+
console.console.print(f"{indent}[dim]•[/dim] [dim]No MCP status available[/dim]")
|
|
693
|
+
return
|
|
694
|
+
|
|
695
|
+
template_expected = False
|
|
696
|
+
if hasattr(agent, "config"):
|
|
697
|
+
template_expected = "{{serverInstructions}}" in str(
|
|
698
|
+
getattr(agent.config, "instruction", "")
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
try:
|
|
702
|
+
total_width = console.console.size.width
|
|
703
|
+
except Exception:
|
|
704
|
+
total_width = 80
|
|
705
|
+
|
|
706
|
+
def render_header(label: Text, right: Text | None = None) -> None:
|
|
707
|
+
line = Text()
|
|
708
|
+
line.append_text(label)
|
|
709
|
+
line.append(" ")
|
|
710
|
+
|
|
711
|
+
separator_width = total_width - line.cell_len
|
|
712
|
+
if right and right.cell_len > 0:
|
|
713
|
+
separator_width -= right.cell_len
|
|
714
|
+
separator_width = max(1, separator_width)
|
|
715
|
+
line.append("─" * separator_width, style="dim")
|
|
716
|
+
line.append_text(right)
|
|
717
|
+
else:
|
|
718
|
+
line.append("─" * max(1, separator_width), style="dim")
|
|
719
|
+
|
|
720
|
+
console.console.print()
|
|
721
|
+
console.console.print(line)
|
|
722
|
+
console.console.print()
|
|
723
|
+
|
|
724
|
+
server_items = list(sorted(server_status_map.items()))
|
|
725
|
+
|
|
726
|
+
for index, (server, status) in enumerate(server_items, start=1):
|
|
727
|
+
primary_caps, secondary_caps = _format_capability_shorthand(status, template_expected)
|
|
728
|
+
|
|
729
|
+
impl_name = status.implementation_name or status.server_name or "unknown"
|
|
730
|
+
impl_display = impl_name[:30]
|
|
731
|
+
if len(impl_name) > 30:
|
|
732
|
+
impl_display = impl_display[:27] + "..."
|
|
733
|
+
|
|
734
|
+
version_display = status.implementation_version or ""
|
|
735
|
+
if len(version_display) > 12:
|
|
736
|
+
version_display = version_display[:9] + "..."
|
|
737
|
+
|
|
738
|
+
header_label = Text(indent)
|
|
739
|
+
header_label.append("▎", style=Colours.TEXT_CYAN)
|
|
740
|
+
header_label.append(SYMBOL_RESPONSE, style=f"dim {Colours.TEXT_CYAN}")
|
|
741
|
+
header_label.append(f" [{index:2}] ", style=Colours.TEXT_CYAN)
|
|
742
|
+
header_label.append(server, style=f"{Colours.TEXT_INFO} bold")
|
|
743
|
+
render_header(header_label)
|
|
744
|
+
|
|
745
|
+
# First line: name and version
|
|
746
|
+
meta_line = Text(indent + " ")
|
|
747
|
+
meta_fields: list[Text] = []
|
|
748
|
+
meta_fields.append(_build_aligned_field("name", impl_display))
|
|
749
|
+
if version_display:
|
|
750
|
+
meta_fields.append(_build_aligned_field("version", version_display))
|
|
751
|
+
|
|
752
|
+
for idx, field in enumerate(meta_fields):
|
|
753
|
+
if idx:
|
|
754
|
+
meta_line.append(" ", style="dim")
|
|
755
|
+
meta_line.append_text(field)
|
|
756
|
+
|
|
757
|
+
client_parts = []
|
|
758
|
+
if status.client_info_name:
|
|
759
|
+
client_parts.append(status.client_info_name)
|
|
760
|
+
if status.client_info_version:
|
|
761
|
+
client_parts.append(status.client_info_version)
|
|
762
|
+
client_display = " ".join(client_parts)
|
|
763
|
+
if len(client_display) > 24:
|
|
764
|
+
client_display = client_display[:21] + "..."
|
|
765
|
+
|
|
766
|
+
if client_display:
|
|
767
|
+
meta_line.append(" | ", style="dim")
|
|
768
|
+
meta_line.append_text(_build_aligned_field("client", client_display))
|
|
769
|
+
|
|
770
|
+
console.console.print(meta_line)
|
|
771
|
+
|
|
772
|
+
# Second line: session (on its own line)
|
|
773
|
+
session_line = Text(indent + " ")
|
|
774
|
+
session_text = _format_session_id(status.session_id)
|
|
775
|
+
session_line.append_text(_build_aligned_field("session", session_text))
|
|
776
|
+
console.console.print(session_line)
|
|
777
|
+
console.console.print()
|
|
778
|
+
|
|
779
|
+
# Build status segments
|
|
780
|
+
state_segments: list[Text] = []
|
|
781
|
+
|
|
782
|
+
duration = _format_compact_duration(status.staleness_seconds)
|
|
783
|
+
if duration:
|
|
784
|
+
last_text = Text("last activity: ", style=Colours.TEXT_DIM)
|
|
785
|
+
last_text.append(duration, style=Colours.TEXT_DEFAULT)
|
|
786
|
+
last_text.append(" ago", style=Colours.TEXT_DIM)
|
|
787
|
+
state_segments.append(last_text)
|
|
788
|
+
|
|
789
|
+
if status.error_message and status.is_connected is False:
|
|
790
|
+
state_segments.append(Text(status.error_message, style=Colours.TEXT_ERROR))
|
|
791
|
+
|
|
792
|
+
instr_available = bool(status.instructions_available)
|
|
793
|
+
if instr_available and status.instructions_enabled is False:
|
|
794
|
+
state_segments.append(Text("instructions disabled", style=Colours.TEXT_ERROR))
|
|
795
|
+
elif instr_available and not template_expected:
|
|
796
|
+
state_segments.append(Text("instr. not in sysprompt", style=Colours.TEXT_WARNING))
|
|
797
|
+
|
|
798
|
+
if status.spoofing_enabled:
|
|
799
|
+
state_segments.append(Text("client spoof", style=Colours.TEXT_WARNING))
|
|
800
|
+
|
|
801
|
+
# Main status line (without transport and connected)
|
|
802
|
+
if state_segments:
|
|
803
|
+
status_line = Text(indent + " ")
|
|
804
|
+
for idx, segment in enumerate(state_segments):
|
|
805
|
+
if idx:
|
|
806
|
+
status_line.append(" | ", style="dim")
|
|
807
|
+
status_line.append_text(segment)
|
|
808
|
+
console.console.print(status_line)
|
|
809
|
+
|
|
810
|
+
# MCP protocol calls made (only shows calls that have actually been invoked)
|
|
811
|
+
calls = _summarise_call_counts(status.call_counts)
|
|
812
|
+
if calls:
|
|
813
|
+
calls_line = Text(indent + " ")
|
|
814
|
+
calls_line.append("mcp calls: ", style=Colours.TEXT_DIM)
|
|
815
|
+
calls_line.append(calls, style=Colours.TEXT_DEFAULT)
|
|
816
|
+
# Show reconnect count inline if > 0
|
|
817
|
+
if status.reconnect_count > 0:
|
|
818
|
+
calls_line.append(" | ", style="dim")
|
|
819
|
+
calls_line.append("reconnects: ", style=Colours.TEXT_DIM)
|
|
820
|
+
calls_line.append(str(status.reconnect_count), style=Colours.TEXT_WARNING)
|
|
821
|
+
console.console.print(calls_line)
|
|
822
|
+
elif status.reconnect_count > 0:
|
|
823
|
+
# Show reconnect count on its own line if no calls
|
|
824
|
+
reconnect_line = Text(indent + " ")
|
|
825
|
+
reconnect_line.append("reconnects: ", style=Colours.TEXT_DIM)
|
|
826
|
+
reconnect_line.append(str(status.reconnect_count), style=Colours.TEXT_WARNING)
|
|
827
|
+
console.console.print(reconnect_line)
|
|
828
|
+
_render_channel_summary(status, indent, total_width)
|
|
829
|
+
|
|
830
|
+
combined_tokens = primary_caps + secondary_caps
|
|
831
|
+
prefix = Text(indent)
|
|
832
|
+
prefix.append("─| ", style="dim")
|
|
833
|
+
suffix = Text(" |", style="dim")
|
|
834
|
+
|
|
835
|
+
caps_content = (
|
|
836
|
+
_build_capability_text(combined_tokens)
|
|
837
|
+
if combined_tokens
|
|
838
|
+
else Text("none", style="dim")
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
caps_display = caps_content.copy()
|
|
842
|
+
available = max(0, total_width - prefix.cell_len - suffix.cell_len)
|
|
843
|
+
if caps_display.cell_len > available:
|
|
844
|
+
caps_display.truncate(available)
|
|
845
|
+
|
|
846
|
+
banner_line = Text()
|
|
847
|
+
banner_line.append_text(prefix)
|
|
848
|
+
banner_line.append_text(caps_display)
|
|
849
|
+
banner_line.append_text(suffix)
|
|
850
|
+
remaining = total_width - banner_line.cell_len
|
|
851
|
+
if remaining > 0:
|
|
852
|
+
banner_line.append("─" * remaining, style="dim")
|
|
853
|
+
|
|
854
|
+
console.console.print(banner_line)
|
|
855
|
+
|
|
856
|
+
if index != len(server_items):
|
|
857
|
+
console.console.print()
|