fast-agent-mcp 0.3.7__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.

Files changed (34) hide show
  1. fast_agent/agents/llm_agent.py +30 -8
  2. fast_agent/agents/llm_decorator.py +2 -7
  3. fast_agent/agents/mcp_agent.py +9 -4
  4. fast_agent/cli/commands/auth.py +14 -1
  5. fast_agent/core/direct_factory.py +20 -8
  6. fast_agent/core/logging/listeners.py +2 -1
  7. fast_agent/interfaces.py +2 -2
  8. fast_agent/llm/model_database.py +7 -1
  9. fast_agent/llm/model_factory.py +2 -3
  10. fast_agent/llm/provider/anthropic/llm_anthropic.py +107 -62
  11. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +4 -3
  12. fast_agent/llm/provider/bedrock/llm_bedrock.py +1 -1
  13. fast_agent/llm/provider/google/google_converter.py +8 -41
  14. fast_agent/llm/provider/google/llm_google_native.py +1 -3
  15. fast_agent/llm/provider/openai/llm_azure.py +1 -1
  16. fast_agent/llm/provider/openai/llm_openai.py +3 -3
  17. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +1 -1
  18. fast_agent/llm/request_params.py +1 -1
  19. fast_agent/mcp/mcp_agent_client_session.py +45 -2
  20. fast_agent/mcp/mcp_aggregator.py +282 -5
  21. fast_agent/mcp/mcp_connection_manager.py +86 -10
  22. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  23. fast_agent/mcp/streamable_http_tracking.py +309 -0
  24. fast_agent/mcp/transport_tracking.py +598 -0
  25. fast_agent/resources/examples/data-analysis/analysis.py +7 -3
  26. fast_agent/ui/console_display.py +22 -1
  27. fast_agent/ui/enhanced_prompt.py +21 -1
  28. fast_agent/ui/interactive_prompt.py +5 -0
  29. fast_agent/ui/mcp_display.py +636 -0
  30. {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/METADATA +6 -6
  31. {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/RECORD +34 -30
  32. {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/WHEEL +0 -0
  33. {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/entry_points.txt +0 -0
  34. {fast_agent_mcp-0.3.7.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.7
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
@@ -209,7 +209,7 @@ Classifier: License :: OSI Approved :: Apache Software License
209
209
  Classifier: Operating System :: OS Independent
210
210
  Classifier: Programming Language :: Python :: 3
211
211
  Requires-Python: >=3.13.5
212
- Requires-Dist: a2a-sdk>=0.3.0
212
+ Requires-Dist: a2a-sdk>=0.3.6
213
213
  Requires-Dist: aiohttp>=3.11.13
214
214
  Requires-Dist: anthropic>=0.68.0
215
215
  Requires-Dist: azure-identity>=1.14.0
@@ -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.14.0
223
- Requires-Dist: openai>=1.108.0
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.51
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 # with a remote 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