fast-agent-mcp 0.3.13__py3-none-any.whl → 0.3.15__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 +59 -37
- fast_agent/agents/llm_decorator.py +13 -2
- fast_agent/agents/mcp_agent.py +21 -5
- fast_agent/agents/tool_agent.py +41 -29
- fast_agent/agents/workflow/router_agent.py +2 -1
- fast_agent/cli/commands/check_config.py +48 -1
- fast_agent/config.py +65 -2
- fast_agent/constants.py +3 -0
- fast_agent/context.py +42 -9
- fast_agent/core/fastagent.py +14 -1
- fast_agent/core/logging/listeners.py +1 -1
- fast_agent/core/validation.py +31 -33
- fast_agent/event_progress.py +2 -3
- fast_agent/human_input/form_fields.py +4 -1
- fast_agent/interfaces.py +12 -2
- fast_agent/llm/fastagent_llm.py +31 -0
- fast_agent/llm/model_database.py +2 -2
- fast_agent/llm/model_factory.py +8 -1
- fast_agent/llm/provider_key_manager.py +1 -0
- fast_agent/llm/provider_types.py +1 -0
- fast_agent/llm/request_params.py +3 -1
- fast_agent/mcp/mcp_aggregator.py +313 -40
- fast_agent/mcp/mcp_connection_manager.py +39 -9
- fast_agent/mcp/prompt_message_extended.py +2 -2
- fast_agent/mcp/skybridge.py +45 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/transport_tracking.py +37 -3
- fast_agent/mcp/types.py +24 -0
- fast_agent/resources/examples/workflows/router.py +1 -0
- fast_agent/resources/setup/fastagent.config.yaml +7 -1
- fast_agent/ui/console_display.py +946 -84
- fast_agent/ui/elicitation_form.py +23 -1
- fast_agent/ui/enhanced_prompt.py +153 -58
- fast_agent/ui/interactive_prompt.py +57 -34
- fast_agent/ui/markdown_truncator.py +942 -0
- fast_agent/ui/mcp_display.py +110 -29
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/rich_progress.py +4 -1
- fast_agent/ui/streaming_buffer.py +449 -0
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/METADATA +4 -3
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/RECORD +44 -38
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/licenses/LICENSE +0 -0
fast_agent/ui/mcp_display.py
CHANGED
|
@@ -39,6 +39,7 @@ class Colours:
|
|
|
39
39
|
# Capability token states
|
|
40
40
|
TOKEN_ERROR = "bright_red"
|
|
41
41
|
TOKEN_WARNING = "bright_cyan"
|
|
42
|
+
TOKEN_CAUTION = "bright_yellow"
|
|
42
43
|
TOKEN_DISABLED = "dim"
|
|
43
44
|
TOKEN_HIGHLIGHTED = "bright_yellow"
|
|
44
45
|
TOKEN_ENABLED = "bright_green"
|
|
@@ -104,6 +105,38 @@ def _format_compact_duration(seconds: float | None) -> str | None:
|
|
|
104
105
|
return f"{days}d{hours:02d}h"
|
|
105
106
|
|
|
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
|
+
|
|
107
140
|
def _summarise_call_counts(call_counts: dict[str, int]) -> str | None:
|
|
108
141
|
if not call_counts:
|
|
109
142
|
return None
|
|
@@ -192,7 +225,7 @@ def _format_capability_shorthand(
|
|
|
192
225
|
elif instructions_enabled is False:
|
|
193
226
|
entries.append(("In", "red", False))
|
|
194
227
|
elif instructions_enabled is None and not template_expected:
|
|
195
|
-
entries.append(("In", "
|
|
228
|
+
entries.append(("In", "warn", False))
|
|
196
229
|
elif instructions_enabled is None:
|
|
197
230
|
entries.append(("In", True, False))
|
|
198
231
|
elif template_expected:
|
|
@@ -200,6 +233,18 @@ def _format_capability_shorthand(
|
|
|
200
233
|
else:
|
|
201
234
|
entries.append(("In", "blue", False))
|
|
202
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
|
+
|
|
203
248
|
if status.roots_configured:
|
|
204
249
|
entries.append(("Ro", True, False))
|
|
205
250
|
else:
|
|
@@ -228,6 +273,8 @@ def _format_capability_shorthand(
|
|
|
228
273
|
return Colours.TOKEN_ERROR
|
|
229
274
|
if supported == "blue":
|
|
230
275
|
return Colours.TOKEN_WARNING
|
|
276
|
+
if supported == "warn":
|
|
277
|
+
return Colours.TOKEN_CAUTION
|
|
231
278
|
if not supported:
|
|
232
279
|
return Colours.TOKEN_DISABLED
|
|
233
280
|
if highlighted:
|
|
@@ -268,10 +315,28 @@ def _format_label(label: str, width: int = 10) -> str:
|
|
|
268
315
|
return f"{label:<{width}}" if len(label) < width else label
|
|
269
316
|
|
|
270
317
|
|
|
271
|
-
def _build_inline_timeline(
|
|
318
|
+
def _build_inline_timeline(
|
|
319
|
+
buckets: Iterable[str],
|
|
320
|
+
*,
|
|
321
|
+
bucket_seconds: int | None = None,
|
|
322
|
+
bucket_count: int | None = None,
|
|
323
|
+
) -> str:
|
|
272
324
|
"""Build a compact timeline string for inline display."""
|
|
273
|
-
|
|
274
|
-
|
|
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:
|
|
275
340
|
color = TIMELINE_COLORS.get(state, Colours.NONE)
|
|
276
341
|
if state in {"idle", "none"}:
|
|
277
342
|
symbol = SYMBOL_IDLE
|
|
@@ -297,6 +362,10 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
|
|
|
297
362
|
if snapshot is None:
|
|
298
363
|
return
|
|
299
364
|
|
|
365
|
+
transport_value = getattr(status, "transport", None)
|
|
366
|
+
transport_lower = (transport_value or "").lower()
|
|
367
|
+
is_sse_transport = transport_lower == "sse"
|
|
368
|
+
|
|
300
369
|
# Show channel types based on what's available
|
|
301
370
|
entries: list[tuple[str, str, ChannelSnapshot | None]] = []
|
|
302
371
|
|
|
@@ -311,12 +380,13 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
|
|
|
311
380
|
stdio_channel = getattr(snapshot, "stdio", None)
|
|
312
381
|
|
|
313
382
|
if any(channel is not None for channel in http_channels):
|
|
314
|
-
# HTTP transport - show
|
|
383
|
+
# HTTP or SSE transport - show available channels
|
|
315
384
|
entries = [
|
|
316
385
|
("GET (SSE)", "◀", getattr(snapshot, "get", None)),
|
|
317
386
|
("POST (SSE)", "▶", getattr(snapshot, "post_sse", None)),
|
|
318
|
-
("POST (JSON)", "▶", getattr(snapshot, "post_json", None)),
|
|
319
387
|
]
|
|
388
|
+
if not is_sse_transport:
|
|
389
|
+
entries.append(("POST (JSON)", "▶", getattr(snapshot, "post_json", None)))
|
|
320
390
|
elif stdio_channel is not None:
|
|
321
391
|
# STDIO transport - show single bidirectional channel
|
|
322
392
|
entries = [
|
|
@@ -332,36 +402,31 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
|
|
|
332
402
|
# Determine if we're showing stdio or HTTP channels
|
|
333
403
|
is_stdio = stdio_channel is not None
|
|
334
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
|
+
|
|
335
413
|
# Get transport type for display
|
|
336
|
-
transport =
|
|
414
|
+
transport = transport_value or "unknown"
|
|
337
415
|
transport_display = transport.upper() if transport != "unknown" else "Channels"
|
|
338
416
|
|
|
339
417
|
# Header with column labels
|
|
340
418
|
header = Text(indent)
|
|
341
|
-
|
|
419
|
+
header_intro = f"┌ {transport_display} "
|
|
420
|
+
header.append(header_intro, style="dim")
|
|
342
421
|
|
|
343
422
|
# Calculate padding needed based on transport display length
|
|
344
|
-
|
|
345
|
-
header_prefix_len = 3 + len(transport_display)
|
|
423
|
+
header_prefix_len = len(header_intro)
|
|
346
424
|
|
|
425
|
+
dash_count = max(1, metrics_prefix_width - header_prefix_len + 2)
|
|
347
426
|
if is_stdio:
|
|
348
|
-
# Simplified header for stdio: just activity column
|
|
349
|
-
# Need to align with "│ ⇄ STDIO 10m ●●●●●●●●●●●●●●●●●●●● now 29"
|
|
350
|
-
# That's: "│ " + arrow + " " + label(13) + "10m " + dots(20) + " now" = 47 chars
|
|
351
|
-
# Then: " " + activity(8) = 10 chars
|
|
352
|
-
# Total content width = 47 + 10 = 57 chars
|
|
353
|
-
# So we need 47 - header_prefix_len dashes before "activity"
|
|
354
|
-
dash_count = max(1, 47 - header_prefix_len)
|
|
355
427
|
header.append("─" * dash_count, style="dim")
|
|
356
428
|
header.append(" activity", style="dim")
|
|
357
429
|
else:
|
|
358
|
-
# Original header for HTTP channels
|
|
359
|
-
# Need to align with the req/resp/notif/ping columns
|
|
360
|
-
# Structure: "│ " + arrow + " " + label(13) + "10m " + dots(20) + " now" = 47 chars
|
|
361
|
-
# Then: " " + req(5) + " " + resp(5) + " " + notif(5) + " " + ping(5) = 25 chars
|
|
362
|
-
# Total content width = 47 + 25 = 72 chars
|
|
363
|
-
# So we need 47 - header_prefix_len dashes before the column headers
|
|
364
|
-
dash_count = max(1, 47 - header_prefix_len)
|
|
365
430
|
header.append("─" * dash_count, style="dim")
|
|
366
431
|
header.append(" req resp notif ping", style="dim")
|
|
367
432
|
|
|
@@ -449,10 +514,24 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
|
|
|
449
514
|
line.append(f" {label:<13}", style=label_style)
|
|
450
515
|
|
|
451
516
|
# Always show timeline (dim black dots if no data)
|
|
452
|
-
|
|
453
|
-
|
|
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:
|
|
454
533
|
# Show actual activity
|
|
455
|
-
for bucket_state in
|
|
534
|
+
for bucket_state in bucket_states:
|
|
456
535
|
color = timeline_color_map.get(bucket_state, "dim")
|
|
457
536
|
if bucket_state in {"idle", "none"}:
|
|
458
537
|
symbol = SYMBOL_IDLE
|
|
@@ -473,7 +552,7 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
|
|
|
473
552
|
line.append(symbol, style=f"bold {color}")
|
|
474
553
|
else:
|
|
475
554
|
# Show dim dots for no activity
|
|
476
|
-
for _ in range(
|
|
555
|
+
for _ in range(bucket_count):
|
|
477
556
|
line.append(SYMBOL_IDLE, style="black dim")
|
|
478
557
|
line.append(" now", style="dim")
|
|
479
558
|
|
|
@@ -588,6 +667,8 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
|
|
|
588
667
|
symbol = SYMBOL_ERROR
|
|
589
668
|
elif name == "ping":
|
|
590
669
|
symbol = SYMBOL_PING
|
|
670
|
+
elif is_stdio and name == "activity":
|
|
671
|
+
symbol = SYMBOL_STDIO_ACTIVITY
|
|
591
672
|
else:
|
|
592
673
|
symbol = SYMBOL_RESPONSE
|
|
593
674
|
footer.append(symbol, style=f"{color}")
|
|
@@ -712,7 +793,7 @@ async def render_mcp_status(agent, indent: str = "") -> None:
|
|
|
712
793
|
if instr_available and status.instructions_enabled is False:
|
|
713
794
|
state_segments.append(Text("instructions disabled", style=Colours.TEXT_ERROR))
|
|
714
795
|
elif instr_available and not template_expected:
|
|
715
|
-
state_segments.append(Text("
|
|
796
|
+
state_segments.append(Text("instr. not in sysprompt", style=Colours.TEXT_WARNING))
|
|
716
797
|
|
|
717
798
|
if status.spoofing_enabled:
|
|
718
799
|
state_segments.append(Text("client spoof", style=Colours.TEXT_WARNING))
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""High performance truncation for plain text streaming displays."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PlainTextTruncator:
|
|
9
|
+
"""Trim plain text content to fit within a target terminal window."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, target_height_ratio: float = 0.7) -> None:
|
|
12
|
+
if not 0 < target_height_ratio <= 1:
|
|
13
|
+
raise ValueError("target_height_ratio must be between 0 and 1")
|
|
14
|
+
self.target_height_ratio = target_height_ratio
|
|
15
|
+
|
|
16
|
+
def truncate(self, text: str, *, terminal_height: int, terminal_width: int) -> str:
|
|
17
|
+
"""Return the most recent portion of text that fits the terminal window.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
text: Full text buffer accumulated during streaming.
|
|
21
|
+
terminal_height: Terminal height in rows.
|
|
22
|
+
terminal_width: Terminal width in columns.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Tail portion of the text that fits within the target height ratio.
|
|
26
|
+
"""
|
|
27
|
+
if not text:
|
|
28
|
+
return text
|
|
29
|
+
|
|
30
|
+
if terminal_height <= 0 or terminal_width <= 0:
|
|
31
|
+
return text
|
|
32
|
+
|
|
33
|
+
target_rows = max(1, int(terminal_height * self.target_height_ratio))
|
|
34
|
+
width = max(1, terminal_width)
|
|
35
|
+
|
|
36
|
+
idx = len(text)
|
|
37
|
+
rows_used = 0
|
|
38
|
+
start_idx = 0
|
|
39
|
+
|
|
40
|
+
while idx > 0 and rows_used < target_rows:
|
|
41
|
+
prev_newline = text.rfind("\n", 0, idx)
|
|
42
|
+
line_start = prev_newline + 1 if prev_newline != -1 else 0
|
|
43
|
+
line = text[line_start:idx]
|
|
44
|
+
expanded = line.expandtabs()
|
|
45
|
+
line_len = len(expanded)
|
|
46
|
+
line_rows = max(1, math.ceil(line_len / width)) if line_len else 1
|
|
47
|
+
|
|
48
|
+
if rows_used + line_rows >= target_rows:
|
|
49
|
+
rows_remaining = target_rows - rows_used
|
|
50
|
+
if rows_remaining <= 0:
|
|
51
|
+
start_idx = idx
|
|
52
|
+
break
|
|
53
|
+
|
|
54
|
+
if line_rows <= rows_remaining:
|
|
55
|
+
start_idx = line_start
|
|
56
|
+
else:
|
|
57
|
+
approx_chars = width * rows_remaining
|
|
58
|
+
keep_chars = min(len(line), approx_chars)
|
|
59
|
+
start_idx = idx - keep_chars
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
rows_used += line_rows
|
|
63
|
+
start_idx = line_start
|
|
64
|
+
if prev_newline == -1:
|
|
65
|
+
break
|
|
66
|
+
idx = prev_newline
|
|
67
|
+
|
|
68
|
+
return text[start_idx:]
|
fast_agent/ui/rich_progress.py
CHANGED
|
@@ -78,6 +78,7 @@ class RichProgressDisplay:
|
|
|
78
78
|
ProgressAction.INITIALIZED: "dim green",
|
|
79
79
|
ProgressAction.CHATTING: "bold blue",
|
|
80
80
|
ProgressAction.STREAMING: "bold green", # Assistant Colour
|
|
81
|
+
ProgressAction.THINKING: "bold yellow", # Assistant Colour
|
|
81
82
|
ProgressAction.ROUTING: "bold blue",
|
|
82
83
|
ProgressAction.PLANNING: "bold blue",
|
|
83
84
|
ProgressAction.READY: "dim green",
|
|
@@ -108,7 +109,9 @@ class RichProgressDisplay:
|
|
|
108
109
|
|
|
109
110
|
# Ensure no None values in the update
|
|
110
111
|
# For streaming, use custom description immediately to avoid flashing
|
|
111
|
-
if
|
|
112
|
+
if (
|
|
113
|
+
event.action == ProgressAction.STREAMING or event.action == ProgressAction.THINKING
|
|
114
|
+
) and event.streaming_tokens:
|
|
112
115
|
# Account for [dim][/dim] tags (11 characters) in padding calculation
|
|
113
116
|
formatted_tokens = f"▎[dim]◀[/dim] {event.streaming_tokens.strip()}".ljust(17 + 11)
|
|
114
117
|
description = f"[{self._get_action_style(event.action)}]{formatted_tokens}"
|