fast-agent-mcp 0.3.8__py3-none-any.whl → 0.3.9__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.
Potentially problematic release.
This version of fast-agent-mcp might be problematic. Click here for more details.
- fast_agent/agents/llm_agent.py +24 -0
- fast_agent/agents/mcp_agent.py +7 -1
- fast_agent/core/direct_factory.py +20 -8
- fast_agent/llm/provider/anthropic/llm_anthropic.py +107 -62
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +4 -3
- fast_agent/llm/provider/google/google_converter.py +8 -41
- fast_agent/llm/provider/openai/llm_openai.py +3 -3
- fast_agent/mcp/mcp_agent_client_session.py +45 -2
- fast_agent/mcp/mcp_aggregator.py +282 -5
- fast_agent/mcp/mcp_connection_manager.py +86 -10
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/transport_tracking.py +598 -0
- fast_agent/resources/examples/data-analysis/analysis.py +7 -3
- fast_agent/ui/console_display.py +22 -1
- fast_agent/ui/enhanced_prompt.py +21 -1
- fast_agent/ui/interactive_prompt.py +5 -0
- fast_agent/ui/mcp_display.py +636 -0
- {fast_agent_mcp-0.3.8.dist-info → fast_agent_mcp-0.3.9.dist-info}/METADATA +5 -5
- {fast_agent_mcp-0.3.8.dist-info → fast_agent_mcp-0.3.9.dist-info}/RECORD +23 -19
- {fast_agent_mcp-0.3.8.dist-info → fast_agent_mcp-0.3.9.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.8.dist-info → fast_agent_mcp-0.3.9.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.8.dist-info → fast_agent_mcp-0.3.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,636 @@
|
|
|
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
|
+
def _format_compact_duration(seconds: float | None) -> str | None:
|
|
18
|
+
if seconds is None:
|
|
19
|
+
return None
|
|
20
|
+
total = int(seconds)
|
|
21
|
+
if total < 1:
|
|
22
|
+
return "<1s"
|
|
23
|
+
mins, secs = divmod(total, 60)
|
|
24
|
+
if mins == 0:
|
|
25
|
+
return f"{secs}s"
|
|
26
|
+
hours, mins = divmod(mins, 60)
|
|
27
|
+
if hours == 0:
|
|
28
|
+
return f"{mins}m{secs:02d}s"
|
|
29
|
+
days, hours = divmod(hours, 24)
|
|
30
|
+
if days == 0:
|
|
31
|
+
return f"{hours}h{mins:02d}m"
|
|
32
|
+
return f"{days}d{hours:02d}h"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _summarise_call_counts(call_counts: dict[str, int]) -> str | None:
|
|
36
|
+
if not call_counts:
|
|
37
|
+
return None
|
|
38
|
+
ordered = sorted(call_counts.items(), key=lambda item: item[0])
|
|
39
|
+
return ", ".join(f"{name}:{count}" for name, count in ordered)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _format_session_id(session_id: str | None) -> Text:
|
|
43
|
+
text = Text()
|
|
44
|
+
if not session_id:
|
|
45
|
+
text.append("none", style="yellow")
|
|
46
|
+
return text
|
|
47
|
+
if session_id == "local":
|
|
48
|
+
text.append("local", style="cyan")
|
|
49
|
+
return text
|
|
50
|
+
|
|
51
|
+
# Only trim if excessively long (>24 chars)
|
|
52
|
+
value = session_id
|
|
53
|
+
if len(session_id) > 24:
|
|
54
|
+
# Trim middle to preserve start and end
|
|
55
|
+
value = f"{session_id[:10]}...{session_id[-10:]}"
|
|
56
|
+
text.append(value, style="green")
|
|
57
|
+
return text
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _build_aligned_field(
|
|
61
|
+
label: str, value: Text | str, *, label_width: int = 9, value_style: str = "white"
|
|
62
|
+
) -> Text:
|
|
63
|
+
field = Text()
|
|
64
|
+
field.append(f"{label:<{label_width}}: ", style="dim")
|
|
65
|
+
if isinstance(value, Text):
|
|
66
|
+
field.append_text(value)
|
|
67
|
+
else:
|
|
68
|
+
field.append(value, style=value_style)
|
|
69
|
+
return field
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _cap_attr(source, attr: str | None) -> bool:
|
|
73
|
+
if source is None:
|
|
74
|
+
return False
|
|
75
|
+
target = source
|
|
76
|
+
if attr:
|
|
77
|
+
if isinstance(source, dict):
|
|
78
|
+
target = source.get(attr)
|
|
79
|
+
else:
|
|
80
|
+
target = getattr(source, attr, None)
|
|
81
|
+
if isinstance(target, bool):
|
|
82
|
+
return target
|
|
83
|
+
return bool(target)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _format_capability_shorthand(
|
|
87
|
+
status: ServerStatus, template_expected: bool
|
|
88
|
+
) -> tuple[list[tuple[str, str]], list[tuple[str, str]]]:
|
|
89
|
+
caps = status.server_capabilities
|
|
90
|
+
tools = getattr(caps, "tools", None)
|
|
91
|
+
prompts = getattr(caps, "prompts", None)
|
|
92
|
+
resources = getattr(caps, "resources", None)
|
|
93
|
+
logging_caps = getattr(caps, "logging", None)
|
|
94
|
+
completion_caps = (
|
|
95
|
+
getattr(caps, "completion", None)
|
|
96
|
+
or getattr(caps, "completions", None)
|
|
97
|
+
or getattr(caps, "respond", None)
|
|
98
|
+
)
|
|
99
|
+
experimental_caps = getattr(caps, "experimental", None)
|
|
100
|
+
|
|
101
|
+
instructions_available = bool(status.instructions_available)
|
|
102
|
+
instructions_enabled = status.instructions_enabled
|
|
103
|
+
|
|
104
|
+
entries = [
|
|
105
|
+
("To", _cap_attr(tools, None), _cap_attr(tools, "listChanged")),
|
|
106
|
+
("Pr", _cap_attr(prompts, None), _cap_attr(prompts, "listChanged")),
|
|
107
|
+
(
|
|
108
|
+
"Re",
|
|
109
|
+
_cap_attr(resources, "read") or _cap_attr(resources, None),
|
|
110
|
+
_cap_attr(resources, "listChanged"),
|
|
111
|
+
),
|
|
112
|
+
("Rs", _cap_attr(resources, "subscribe"), _cap_attr(resources, "subscribe")),
|
|
113
|
+
("Lo", _cap_attr(logging_caps, None), False),
|
|
114
|
+
("Co", _cap_attr(completion_caps, None), _cap_attr(completion_caps, "listChanged")),
|
|
115
|
+
("Ex", _cap_attr(experimental_caps, None), False),
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
if not instructions_available:
|
|
119
|
+
entries.append(("In", False, False))
|
|
120
|
+
elif instructions_enabled is False:
|
|
121
|
+
entries.append(("In", "red", False))
|
|
122
|
+
elif instructions_enabled is None and not template_expected:
|
|
123
|
+
entries.append(("In", "blue", False))
|
|
124
|
+
elif instructions_enabled is None:
|
|
125
|
+
entries.append(("In", True, False))
|
|
126
|
+
elif template_expected:
|
|
127
|
+
entries.append(("In", True, False))
|
|
128
|
+
else:
|
|
129
|
+
entries.append(("In", "blue", False))
|
|
130
|
+
|
|
131
|
+
if status.roots_configured:
|
|
132
|
+
entries.append(("Ro", True, False))
|
|
133
|
+
else:
|
|
134
|
+
entries.append(("Ro", False, False))
|
|
135
|
+
|
|
136
|
+
mode = (status.elicitation_mode or "").lower()
|
|
137
|
+
if mode == "auto_cancel":
|
|
138
|
+
entries.append(("El", "red", False))
|
|
139
|
+
elif mode and mode != "none":
|
|
140
|
+
entries.append(("El", True, False))
|
|
141
|
+
else:
|
|
142
|
+
entries.append(("El", False, False))
|
|
143
|
+
|
|
144
|
+
sampling_mode = (status.sampling_mode or "").lower()
|
|
145
|
+
if sampling_mode == "configured":
|
|
146
|
+
entries.append(("Sa", "blue", False))
|
|
147
|
+
elif sampling_mode == "auto":
|
|
148
|
+
entries.append(("Sa", True, False))
|
|
149
|
+
else:
|
|
150
|
+
entries.append(("Sa", False, False))
|
|
151
|
+
|
|
152
|
+
entries.append(("Sp", bool(status.spoofing_enabled), False))
|
|
153
|
+
|
|
154
|
+
def token_style(supported, highlighted) -> str:
|
|
155
|
+
if supported == "red":
|
|
156
|
+
return "bright_red"
|
|
157
|
+
if supported == "blue":
|
|
158
|
+
return "bright_cyan"
|
|
159
|
+
if not supported:
|
|
160
|
+
return "dim"
|
|
161
|
+
if highlighted:
|
|
162
|
+
return "bright_yellow"
|
|
163
|
+
return "bright_green"
|
|
164
|
+
|
|
165
|
+
tokens = [
|
|
166
|
+
(label, token_style(supported, highlighted)) for label, supported, highlighted in entries
|
|
167
|
+
]
|
|
168
|
+
return tokens[:8], tokens[8:]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _build_capability_text(tokens: list[tuple[str, str]]) -> Text:
|
|
172
|
+
line = Text()
|
|
173
|
+
host_boundary_inserted = False
|
|
174
|
+
for idx, (label, style) in enumerate(tokens):
|
|
175
|
+
if idx:
|
|
176
|
+
line.append(" ")
|
|
177
|
+
if not host_boundary_inserted and label == "Ro":
|
|
178
|
+
line.append("• ", style="dim")
|
|
179
|
+
host_boundary_inserted = True
|
|
180
|
+
line.append(label, style=style)
|
|
181
|
+
return line
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _format_relative_time(dt: datetime | None) -> str:
|
|
185
|
+
if dt is None:
|
|
186
|
+
return "never"
|
|
187
|
+
try:
|
|
188
|
+
now = datetime.now(timezone.utc)
|
|
189
|
+
except Exception:
|
|
190
|
+
now = datetime.utcnow().replace(tzinfo=timezone.utc)
|
|
191
|
+
seconds = max(0, (now - dt).total_seconds())
|
|
192
|
+
return _format_compact_duration(seconds) or "<1s"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _format_label(label: str, width: int = 10) -> str:
|
|
196
|
+
return f"{label:<{width}}" if len(label) < width else label
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _build_inline_timeline(buckets: Iterable[str]) -> str:
|
|
200
|
+
"""Build a compact timeline string for inline display."""
|
|
201
|
+
color_map = {
|
|
202
|
+
"error": "bright_red",
|
|
203
|
+
"disabled": "bright_blue",
|
|
204
|
+
"response": "bright_blue",
|
|
205
|
+
"request": "bright_yellow",
|
|
206
|
+
"notification": "bright_cyan",
|
|
207
|
+
"ping": "bright_green",
|
|
208
|
+
"none": "dim",
|
|
209
|
+
}
|
|
210
|
+
timeline = " [dim]10m[/dim] "
|
|
211
|
+
for state in buckets:
|
|
212
|
+
color = color_map.get(state, "dim")
|
|
213
|
+
timeline += f"[bold {color}]●[/bold {color}]"
|
|
214
|
+
timeline += " [dim]now[/dim]"
|
|
215
|
+
return timeline
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _render_channel_summary(status: ServerStatus, indent: str, total_width: int) -> None:
|
|
219
|
+
snapshot = getattr(status, "transport_channels", None)
|
|
220
|
+
if snapshot is None:
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
# Show channel types based on what's available
|
|
224
|
+
entries: list[tuple[str, str, ChannelSnapshot | None]] = []
|
|
225
|
+
|
|
226
|
+
# Check if we have HTTP transport channels
|
|
227
|
+
http_channels = [
|
|
228
|
+
getattr(snapshot, "get", None),
|
|
229
|
+
getattr(snapshot, "post_sse", None),
|
|
230
|
+
getattr(snapshot, "post_json", None),
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
# Check if we have stdio transport channel
|
|
234
|
+
stdio_channel = getattr(snapshot, "stdio", None)
|
|
235
|
+
|
|
236
|
+
if any(channel is not None for channel in http_channels):
|
|
237
|
+
# HTTP transport - show the original three channels
|
|
238
|
+
entries = [
|
|
239
|
+
("GET (SSE)", "◀", getattr(snapshot, "get", None)),
|
|
240
|
+
("POST (SSE)", "▶", getattr(snapshot, "post_sse", None)),
|
|
241
|
+
("POST (JSON)", "▶", getattr(snapshot, "post_json", None)),
|
|
242
|
+
]
|
|
243
|
+
elif stdio_channel is not None:
|
|
244
|
+
# STDIO transport - show single bidirectional channel
|
|
245
|
+
entries = [
|
|
246
|
+
("STDIO", "⇄", stdio_channel),
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
# Skip if no channels have data
|
|
250
|
+
if not any(channel is not None for _, _, channel in entries):
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
console.console.print() # Add space before channels
|
|
254
|
+
|
|
255
|
+
# Determine if we're showing stdio or HTTP channels
|
|
256
|
+
is_stdio = stdio_channel is not None
|
|
257
|
+
|
|
258
|
+
# Get transport type for display
|
|
259
|
+
transport = getattr(status, "transport", None) or "unknown"
|
|
260
|
+
transport_display = transport.upper() if transport != "unknown" else "Channels"
|
|
261
|
+
|
|
262
|
+
# Header with column labels
|
|
263
|
+
header = Text(indent)
|
|
264
|
+
header.append(f"┌ {transport_display} ", style="dim")
|
|
265
|
+
|
|
266
|
+
# Calculate padding needed based on transport display length
|
|
267
|
+
# Base structure: "┌ " (2) + transport_display + " " (1) + "─" padding to align with columns
|
|
268
|
+
header_prefix_len = 3 + len(transport_display)
|
|
269
|
+
|
|
270
|
+
if is_stdio:
|
|
271
|
+
# Simplified header for stdio: just activity column
|
|
272
|
+
# Need to align with "│ ⇄ STDIO 10m ●●●●●●●●●●●●●●●●●●●● now 29"
|
|
273
|
+
# That's: "│ " + arrow + " " + label(13) + "10m " + dots(20) + " now" = 47 chars
|
|
274
|
+
# Then: " " + activity(8) = 10 chars
|
|
275
|
+
# Total content width = 47 + 10 = 57 chars
|
|
276
|
+
# So we need 47 - header_prefix_len dashes before "activity"
|
|
277
|
+
dash_count = max(1, 47 - header_prefix_len)
|
|
278
|
+
header.append("─" * dash_count, style="dim")
|
|
279
|
+
header.append(" activity", style="dim")
|
|
280
|
+
else:
|
|
281
|
+
# Original header for HTTP channels
|
|
282
|
+
# Need to align with the req/resp/notif/ping columns
|
|
283
|
+
# Structure: "│ " + arrow + " " + label(13) + "10m " + dots(20) + " now" = 47 chars
|
|
284
|
+
# Then: " " + req(5) + " " + resp(5) + " " + notif(5) + " " + ping(5) = 25 chars
|
|
285
|
+
# Total content width = 47 + 25 = 72 chars
|
|
286
|
+
# So we need 47 - header_prefix_len dashes before the column headers
|
|
287
|
+
dash_count = max(1, 47 - header_prefix_len)
|
|
288
|
+
header.append("─" * dash_count, style="dim")
|
|
289
|
+
header.append(" req resp notif ping", style="dim")
|
|
290
|
+
|
|
291
|
+
console.console.print(header)
|
|
292
|
+
|
|
293
|
+
# Empty row after header for cleaner spacing
|
|
294
|
+
empty_header = Text(indent)
|
|
295
|
+
empty_header.append("│", style="dim")
|
|
296
|
+
console.console.print(empty_header)
|
|
297
|
+
|
|
298
|
+
# Collect any errors to show at bottom
|
|
299
|
+
errors = []
|
|
300
|
+
|
|
301
|
+
# Build timeline color map
|
|
302
|
+
if is_stdio:
|
|
303
|
+
# Simplified color map for stdio: bright green for activity, dim for idle
|
|
304
|
+
timeline_color_map = {
|
|
305
|
+
"error": "bright_red", # Keep error as red
|
|
306
|
+
"request": "bright_green", # All activity shows as bright green
|
|
307
|
+
"response": "bright_green", # (not used in stdio but just in case)
|
|
308
|
+
"notification": "bright_green", # (not used in stdio but just in case)
|
|
309
|
+
"ping": "bright_green", # (not used in stdio but just in case)
|
|
310
|
+
"none": "white dim",
|
|
311
|
+
}
|
|
312
|
+
else:
|
|
313
|
+
# Full color map for HTTP channels
|
|
314
|
+
timeline_color_map = {
|
|
315
|
+
"error": "bright_red",
|
|
316
|
+
"disabled": "bright_blue",
|
|
317
|
+
"response": "bright_blue",
|
|
318
|
+
"request": "bright_yellow",
|
|
319
|
+
"notification": "bright_cyan",
|
|
320
|
+
"ping": "bright_green",
|
|
321
|
+
"none": "white dim",
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for label, arrow, channel in entries:
|
|
325
|
+
line = Text(indent)
|
|
326
|
+
line.append("│ ", style="dim")
|
|
327
|
+
|
|
328
|
+
# Determine arrow color based on state
|
|
329
|
+
arrow_style = "black dim" # default no channel
|
|
330
|
+
if channel:
|
|
331
|
+
state = (channel.state or "open").lower()
|
|
332
|
+
|
|
333
|
+
# Check for 405 status code (method not allowed = disabled endpoint)
|
|
334
|
+
if channel.last_status_code == 405:
|
|
335
|
+
arrow_style = "bright_yellow"
|
|
336
|
+
# Don't add 405 to errors list - it's just disabled, not an error
|
|
337
|
+
# Error state (non-405 errors)
|
|
338
|
+
elif state == "error":
|
|
339
|
+
arrow_style = "bright_red"
|
|
340
|
+
if channel.last_error and channel.last_status_code != 405:
|
|
341
|
+
error_msg = channel.last_error
|
|
342
|
+
if channel.last_status_code:
|
|
343
|
+
errors.append(
|
|
344
|
+
(label.split()[0], f"{error_msg} ({channel.last_status_code})")
|
|
345
|
+
)
|
|
346
|
+
else:
|
|
347
|
+
errors.append((label.split()[0], error_msg))
|
|
348
|
+
# Explicitly disabled or off
|
|
349
|
+
elif state in {"off", "disabled"}:
|
|
350
|
+
arrow_style = "black dim"
|
|
351
|
+
# No activity (idle)
|
|
352
|
+
elif channel.request_count == 0 and channel.response_count == 0:
|
|
353
|
+
arrow_style = "bright_cyan"
|
|
354
|
+
# Active/connected with activity
|
|
355
|
+
elif state in {"open", "connected"}:
|
|
356
|
+
arrow_style = "bright_green"
|
|
357
|
+
# Fallback for other states
|
|
358
|
+
else:
|
|
359
|
+
arrow_style = "bright_cyan"
|
|
360
|
+
|
|
361
|
+
# Arrow and label with better spacing
|
|
362
|
+
line.append(arrow, style=arrow_style)
|
|
363
|
+
line.append(f" {label:<13}", style="bright_white")
|
|
364
|
+
|
|
365
|
+
# Always show timeline (dim black dots if no data)
|
|
366
|
+
line.append("10m ", style="dim")
|
|
367
|
+
if channel and channel.activity_buckets:
|
|
368
|
+
# Show actual activity
|
|
369
|
+
for bucket_state in channel.activity_buckets:
|
|
370
|
+
color = timeline_color_map.get(bucket_state, "dim")
|
|
371
|
+
line.append("●", style=f"bold {color}")
|
|
372
|
+
else:
|
|
373
|
+
# Show dim black dots for no activity
|
|
374
|
+
for _ in range(20):
|
|
375
|
+
line.append("●", style="black dim")
|
|
376
|
+
line.append(" now", style="dim")
|
|
377
|
+
|
|
378
|
+
# Metrics - different layouts for stdio vs HTTP
|
|
379
|
+
if is_stdio:
|
|
380
|
+
# Simplified activity column for stdio
|
|
381
|
+
if channel and channel.message_count > 0:
|
|
382
|
+
activity = str(channel.message_count).rjust(8)
|
|
383
|
+
activity_style = "bright_white"
|
|
384
|
+
else:
|
|
385
|
+
activity = "-".rjust(8)
|
|
386
|
+
activity_style = "dim"
|
|
387
|
+
line.append(f" {activity}", style=activity_style)
|
|
388
|
+
else:
|
|
389
|
+
# Original HTTP columns
|
|
390
|
+
if channel:
|
|
391
|
+
req = str(channel.request_count).rjust(5)
|
|
392
|
+
resp = str(channel.response_count).rjust(5)
|
|
393
|
+
notif = str(channel.notification_count).rjust(5)
|
|
394
|
+
ping = str(channel.ping_count if channel.ping_count else "-").rjust(5)
|
|
395
|
+
else:
|
|
396
|
+
req = "-".rjust(5)
|
|
397
|
+
resp = "-".rjust(5)
|
|
398
|
+
notif = "-".rjust(5)
|
|
399
|
+
ping = "-".rjust(5)
|
|
400
|
+
line.append(
|
|
401
|
+
f" {req} {resp} {notif} {ping}", style="bright_white" if channel else "dim"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
console.console.print(line)
|
|
405
|
+
|
|
406
|
+
# Debug: print the raw line length
|
|
407
|
+
# import sys
|
|
408
|
+
# print(f"Line length: {len(line.plain)}", file=sys.stderr)
|
|
409
|
+
|
|
410
|
+
# Show errors at bottom if any
|
|
411
|
+
if errors:
|
|
412
|
+
# Empty row before errors
|
|
413
|
+
empty_line = Text(indent)
|
|
414
|
+
empty_line.append("│", style="dim")
|
|
415
|
+
console.console.print(empty_line)
|
|
416
|
+
|
|
417
|
+
for channel_type, error_msg in errors:
|
|
418
|
+
error_line = Text(indent)
|
|
419
|
+
error_line.append("│ ", style="dim")
|
|
420
|
+
error_line.append("⚠ ", style="bright_yellow")
|
|
421
|
+
error_line.append(f"{channel_type}: ", style="bright_white")
|
|
422
|
+
# Truncate long error messages
|
|
423
|
+
if len(error_msg) > 60:
|
|
424
|
+
error_msg = error_msg[:57] + "..."
|
|
425
|
+
error_line.append(error_msg, style="bright_red")
|
|
426
|
+
console.console.print(error_line)
|
|
427
|
+
|
|
428
|
+
# Legend if any timelines shown
|
|
429
|
+
has_timelines = any(channel and channel.activity_buckets for _, _, channel in entries)
|
|
430
|
+
|
|
431
|
+
if has_timelines:
|
|
432
|
+
# Empty row before footer with legend
|
|
433
|
+
empty_before = Text(indent)
|
|
434
|
+
empty_before.append("│", style="dim")
|
|
435
|
+
console.console.print(empty_before)
|
|
436
|
+
|
|
437
|
+
# Footer with legend
|
|
438
|
+
footer = Text(indent)
|
|
439
|
+
footer.append("└", style="dim")
|
|
440
|
+
|
|
441
|
+
if has_timelines:
|
|
442
|
+
footer.append(" legend: ", style="dim")
|
|
443
|
+
|
|
444
|
+
if is_stdio:
|
|
445
|
+
# Simplified legend for stdio: just activity vs idle
|
|
446
|
+
legend_map = [
|
|
447
|
+
("activity", "bright_green"),
|
|
448
|
+
("idle", "white dim"),
|
|
449
|
+
]
|
|
450
|
+
else:
|
|
451
|
+
# Full legend for HTTP channels
|
|
452
|
+
legend_map = [
|
|
453
|
+
("error", "bright_red"),
|
|
454
|
+
("response", "bright_blue"),
|
|
455
|
+
("request", "bright_yellow"),
|
|
456
|
+
("notification", "bright_cyan"),
|
|
457
|
+
("ping", "bright_green"),
|
|
458
|
+
("idle", "white dim"),
|
|
459
|
+
]
|
|
460
|
+
|
|
461
|
+
for i, (name, color) in enumerate(legend_map):
|
|
462
|
+
if i > 0:
|
|
463
|
+
footer.append(" ", style="dim")
|
|
464
|
+
footer.append("●", style=f"bold {color}")
|
|
465
|
+
footer.append(f" {name}", style="dim")
|
|
466
|
+
|
|
467
|
+
console.console.print(footer)
|
|
468
|
+
|
|
469
|
+
# Add blank line for spacing before capabilities
|
|
470
|
+
console.console.print()
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
async def render_mcp_status(agent, indent: str = "") -> None:
|
|
474
|
+
server_status_map = {}
|
|
475
|
+
if hasattr(agent, "get_server_status") and callable(getattr(agent, "get_server_status")):
|
|
476
|
+
try:
|
|
477
|
+
server_status_map = await agent.get_server_status()
|
|
478
|
+
except Exception:
|
|
479
|
+
server_status_map = {}
|
|
480
|
+
|
|
481
|
+
if not server_status_map:
|
|
482
|
+
console.console.print(f"{indent}[dim]•[/dim] [dim]No MCP status available[/dim]")
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
template_expected = False
|
|
486
|
+
if hasattr(agent, "config"):
|
|
487
|
+
template_expected = "{{serverInstructions}}" in str(
|
|
488
|
+
getattr(agent.config, "instruction", "")
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
total_width = console.console.size.width
|
|
493
|
+
except Exception:
|
|
494
|
+
total_width = 80
|
|
495
|
+
|
|
496
|
+
def render_header(label: Text, right: Text | None = None) -> None:
|
|
497
|
+
line = Text()
|
|
498
|
+
line.append_text(label)
|
|
499
|
+
line.append(" ")
|
|
500
|
+
|
|
501
|
+
separator_width = total_width - line.cell_len
|
|
502
|
+
if right and right.cell_len > 0:
|
|
503
|
+
separator_width -= right.cell_len
|
|
504
|
+
separator_width = max(1, separator_width)
|
|
505
|
+
line.append("─" * separator_width, style="dim")
|
|
506
|
+
line.append_text(right)
|
|
507
|
+
else:
|
|
508
|
+
line.append("─" * max(1, separator_width), style="dim")
|
|
509
|
+
|
|
510
|
+
console.console.print()
|
|
511
|
+
console.console.print(line)
|
|
512
|
+
console.console.print()
|
|
513
|
+
|
|
514
|
+
server_items = list(sorted(server_status_map.items()))
|
|
515
|
+
|
|
516
|
+
for index, (server, status) in enumerate(server_items, start=1):
|
|
517
|
+
primary_caps, secondary_caps = _format_capability_shorthand(status, template_expected)
|
|
518
|
+
|
|
519
|
+
impl_name = status.implementation_name or status.server_name or "unknown"
|
|
520
|
+
impl_display = impl_name[:30]
|
|
521
|
+
if len(impl_name) > 30:
|
|
522
|
+
impl_display = impl_display[:27] + "..."
|
|
523
|
+
|
|
524
|
+
version_display = status.implementation_version or ""
|
|
525
|
+
if len(version_display) > 12:
|
|
526
|
+
version_display = version_display[:9] + "..."
|
|
527
|
+
|
|
528
|
+
header_label = Text(indent)
|
|
529
|
+
header_label.append("▎", style="cyan")
|
|
530
|
+
header_label.append("●", style="dim cyan")
|
|
531
|
+
header_label.append(f" [{index:2}] ", style="cyan")
|
|
532
|
+
header_label.append(server, style="bright_blue bold")
|
|
533
|
+
render_header(header_label)
|
|
534
|
+
|
|
535
|
+
# First line: name and version
|
|
536
|
+
meta_line = Text(indent + " ")
|
|
537
|
+
meta_fields: list[Text] = []
|
|
538
|
+
meta_fields.append(_build_aligned_field("name", impl_display))
|
|
539
|
+
if version_display:
|
|
540
|
+
meta_fields.append(_build_aligned_field("version", version_display))
|
|
541
|
+
|
|
542
|
+
for idx, field in enumerate(meta_fields):
|
|
543
|
+
if idx:
|
|
544
|
+
meta_line.append(" ", style="dim")
|
|
545
|
+
meta_line.append_text(field)
|
|
546
|
+
|
|
547
|
+
client_parts = []
|
|
548
|
+
if status.client_info_name:
|
|
549
|
+
client_parts.append(status.client_info_name)
|
|
550
|
+
if status.client_info_version:
|
|
551
|
+
client_parts.append(status.client_info_version)
|
|
552
|
+
client_display = " ".join(client_parts)
|
|
553
|
+
if len(client_display) > 24:
|
|
554
|
+
client_display = client_display[:21] + "..."
|
|
555
|
+
|
|
556
|
+
if client_display:
|
|
557
|
+
meta_line.append(" | ", style="dim")
|
|
558
|
+
meta_line.append_text(_build_aligned_field("client", client_display))
|
|
559
|
+
|
|
560
|
+
console.console.print(meta_line)
|
|
561
|
+
|
|
562
|
+
# Second line: session (on its own line)
|
|
563
|
+
session_line = Text(indent + " ")
|
|
564
|
+
session_text = _format_session_id(status.session_id)
|
|
565
|
+
session_line.append_text(_build_aligned_field("session", session_text))
|
|
566
|
+
console.console.print(session_line)
|
|
567
|
+
console.console.print()
|
|
568
|
+
|
|
569
|
+
# Build status segments
|
|
570
|
+
state_segments: list[Text] = []
|
|
571
|
+
|
|
572
|
+
duration = _format_compact_duration(status.staleness_seconds)
|
|
573
|
+
if duration:
|
|
574
|
+
last_text = Text("last activity: ", style="dim")
|
|
575
|
+
last_text.append(duration, style="bright_white")
|
|
576
|
+
last_text.append(" ago", style="dim")
|
|
577
|
+
state_segments.append(last_text)
|
|
578
|
+
|
|
579
|
+
if status.error_message and status.is_connected is False:
|
|
580
|
+
state_segments.append(Text(status.error_message, style="bright_red"))
|
|
581
|
+
|
|
582
|
+
instr_available = bool(status.instructions_available)
|
|
583
|
+
if instr_available and status.instructions_enabled is False:
|
|
584
|
+
state_segments.append(Text("instructions disabled", style="bright_red"))
|
|
585
|
+
elif instr_available and not template_expected:
|
|
586
|
+
state_segments.append(Text("template missing", style="bright_yellow"))
|
|
587
|
+
|
|
588
|
+
if status.spoofing_enabled:
|
|
589
|
+
state_segments.append(Text("client spoof", style="bright_yellow"))
|
|
590
|
+
|
|
591
|
+
# Main status line (without transport and connected)
|
|
592
|
+
if state_segments:
|
|
593
|
+
status_line = Text(indent + " ")
|
|
594
|
+
for idx, segment in enumerate(state_segments):
|
|
595
|
+
if idx:
|
|
596
|
+
status_line.append(" | ", style="dim")
|
|
597
|
+
status_line.append_text(segment)
|
|
598
|
+
console.console.print(status_line)
|
|
599
|
+
|
|
600
|
+
# MCP protocol calls made (only shows calls that have actually been invoked)
|
|
601
|
+
calls = _summarise_call_counts(status.call_counts)
|
|
602
|
+
if calls:
|
|
603
|
+
calls_line = Text(indent + " ")
|
|
604
|
+
calls_line.append("mcp calls: ", style="dim")
|
|
605
|
+
calls_line.append(calls, style="bright_white")
|
|
606
|
+
console.console.print(calls_line)
|
|
607
|
+
_render_channel_summary(status, indent, total_width)
|
|
608
|
+
|
|
609
|
+
combined_tokens = primary_caps + secondary_caps
|
|
610
|
+
prefix = Text(indent)
|
|
611
|
+
prefix.append("─| ", style="dim")
|
|
612
|
+
suffix = Text(" |", style="dim")
|
|
613
|
+
|
|
614
|
+
caps_content = (
|
|
615
|
+
_build_capability_text(combined_tokens)
|
|
616
|
+
if combined_tokens
|
|
617
|
+
else Text("none", style="dim")
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
caps_display = caps_content.copy()
|
|
621
|
+
available = max(0, total_width - prefix.cell_len - suffix.cell_len)
|
|
622
|
+
if caps_display.cell_len > available:
|
|
623
|
+
caps_display.truncate(available)
|
|
624
|
+
|
|
625
|
+
banner_line = Text()
|
|
626
|
+
banner_line.append_text(prefix)
|
|
627
|
+
banner_line.append_text(caps_display)
|
|
628
|
+
banner_line.append_text(suffix)
|
|
629
|
+
remaining = total_width - banner_line.cell_len
|
|
630
|
+
if remaining > 0:
|
|
631
|
+
banner_line.append("─" * remaining, style="dim")
|
|
632
|
+
|
|
633
|
+
console.console.print(banner_line)
|
|
634
|
+
|
|
635
|
+
if index != len(server_items):
|
|
636
|
+
console.console.print()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fast-agent-mcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: Define, Prompt and Test MCP enabled Agents and Workflows
|
|
5
5
|
Author-email: Shaun Smith <fastagent@llmindset.co.uk>
|
|
6
6
|
License: Apache License
|
|
@@ -219,15 +219,15 @@ Requires-Dist: email-validator>=2.2.0
|
|
|
219
219
|
Requires-Dist: fastapi>=0.115.6
|
|
220
220
|
Requires-Dist: google-genai>=1.33.0
|
|
221
221
|
Requires-Dist: keyring>=24.3.1
|
|
222
|
-
Requires-Dist: mcp==1.
|
|
223
|
-
Requires-Dist: openai>=1.
|
|
222
|
+
Requires-Dist: mcp==1.15.0
|
|
223
|
+
Requires-Dist: openai>=1.109.1
|
|
224
224
|
Requires-Dist: opentelemetry-distro>=0.55b0
|
|
225
225
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.7.0
|
|
226
226
|
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.43.1; python_version >= '3.10' and python_version < '4.0'
|
|
227
227
|
Requires-Dist: opentelemetry-instrumentation-google-genai>=0.3b0
|
|
228
228
|
Requires-Dist: opentelemetry-instrumentation-mcp>=0.43.1; python_version >= '3.10' and python_version < '4.0'
|
|
229
229
|
Requires-Dist: opentelemetry-instrumentation-openai>=0.43.1; python_version >= '3.10' and python_version < '4.0'
|
|
230
|
-
Requires-Dist: prompt-toolkit>=3.0.
|
|
230
|
+
Requires-Dist: prompt-toolkit>=3.0.52
|
|
231
231
|
Requires-Dist: pydantic-settings>=2.7.0
|
|
232
232
|
Requires-Dist: pydantic>=2.10.4
|
|
233
233
|
Requires-Dist: pyperclip>=1.9.0
|
|
@@ -280,7 +280,7 @@ Start by installing the [uv package manager](https://docs.astral.sh/uv/) for Pyt
|
|
|
280
280
|
```bash
|
|
281
281
|
uv pip install fast-agent-mcp # install fast-agent!
|
|
282
282
|
fast-agent go # start an interactive session
|
|
283
|
-
fast-agent go https://hf.co/mcp
|
|
283
|
+
fast-agent go --url https://hf.co/mcp # with a remote MCP
|
|
284
284
|
fast-agent go --model=generic.qwen2.5 # use ollama qwen 2.5
|
|
285
285
|
fast-agent setup # create an example agent and config files
|
|
286
286
|
uv run agent.py # run your first agent
|