fast-agent-mcp 0.4.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,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()