fast-agent-mcp 0.3.12__py3-none-any.whl → 0.3.14__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 (35) hide show
  1. fast_agent/agents/llm_agent.py +15 -34
  2. fast_agent/agents/llm_decorator.py +13 -2
  3. fast_agent/agents/mcp_agent.py +18 -2
  4. fast_agent/agents/tool_agent.py +8 -10
  5. fast_agent/cli/commands/check_config.py +45 -1
  6. fast_agent/config.py +63 -0
  7. fast_agent/constants.py +3 -0
  8. fast_agent/context.py +42 -9
  9. fast_agent/core/logging/listeners.py +1 -1
  10. fast_agent/event_progress.py +2 -3
  11. fast_agent/interfaces.py +9 -2
  12. fast_agent/llm/model_factory.py +4 -0
  13. fast_agent/llm/provider/google/google_converter.py +10 -3
  14. fast_agent/llm/provider_key_manager.py +1 -0
  15. fast_agent/llm/provider_types.py +1 -0
  16. fast_agent/llm/request_params.py +3 -1
  17. fast_agent/mcp/mcp_agent_client_session.py +13 -0
  18. fast_agent/mcp/mcp_aggregator.py +313 -40
  19. fast_agent/mcp/mcp_connection_manager.py +95 -22
  20. fast_agent/mcp/skybridge.py +45 -0
  21. fast_agent/mcp/sse_tracking.py +287 -0
  22. fast_agent/mcp/transport_tracking.py +37 -3
  23. fast_agent/mcp/types.py +24 -0
  24. fast_agent/resources/examples/workflows/router.py +1 -0
  25. fast_agent/resources/setup/fastagent.config.yaml +5 -0
  26. fast_agent/ui/console_display.py +347 -20
  27. fast_agent/ui/enhanced_prompt.py +107 -58
  28. fast_agent/ui/interactive_prompt.py +57 -34
  29. fast_agent/ui/mcp_display.py +159 -41
  30. fast_agent/ui/rich_progress.py +4 -1
  31. {fast_agent_mcp-0.3.12.dist-info → fast_agent_mcp-0.3.14.dist-info}/METADATA +16 -7
  32. {fast_agent_mcp-0.3.12.dist-info → fast_agent_mcp-0.3.14.dist-info}/RECORD +35 -32
  33. {fast_agent_mcp-0.3.12.dist-info → fast_agent_mcp-0.3.14.dist-info}/WHEEL +0 -0
  34. {fast_agent_mcp-0.3.12.dist-info → fast_agent_mcp-0.3.14.dist-info}/entry_points.txt +0 -0
  35. {fast_agent_mcp-0.3.12.dist-info → fast_agent_mcp-0.3.14.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,12 @@
1
1
  from enum import Enum
2
2
  from json import JSONDecodeError
3
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Set, Tuple, Union
4
4
 
5
5
  from mcp.types import CallToolResult
6
6
  from rich.panel import Panel
7
7
  from rich.text import Text
8
8
 
9
+ from fast_agent.constants import REASONING
9
10
  from fast_agent.ui import console
10
11
  from fast_agent.ui.mcp_ui_utils import UILink
11
12
  from fast_agent.ui.mermaid_utils import (
@@ -17,6 +18,7 @@ from fast_agent.ui.mermaid_utils import (
17
18
 
18
19
  if TYPE_CHECKING:
19
20
  from fast_agent.mcp.prompt_message_extended import PromptMessageExtended
21
+ from fast_agent.mcp.skybridge import SkybridgeServerConfig
20
22
 
21
23
  CODE_STYLE = "native"
22
24
 
@@ -144,6 +146,25 @@ class ConsoleDisplay:
144
146
  self._markup = config.logger.enable_markup if config else True
145
147
  self._escape_xml = True
146
148
 
149
+ @staticmethod
150
+ def _format_elapsed(elapsed: float) -> str:
151
+ """Format elapsed seconds for display."""
152
+ if elapsed < 0:
153
+ elapsed = 0.0
154
+ if elapsed < 0.001:
155
+ return "<1ms"
156
+ if elapsed < 1:
157
+ return f"{elapsed * 1000:.0f}ms"
158
+ if elapsed < 10:
159
+ return f"{elapsed:.2f}s"
160
+ if elapsed < 60:
161
+ return f"{elapsed:.1f}s"
162
+ minutes, seconds = divmod(elapsed, 60)
163
+ if minutes < 60:
164
+ return f"{int(minutes)}m {seconds:02.0f}s"
165
+ hours, minutes = divmod(int(minutes), 60)
166
+ return f"{hours}h {minutes:02d}m"
167
+
147
168
  def display_message(
148
169
  self,
149
170
  content: Any,
@@ -156,6 +177,7 @@ class ConsoleDisplay:
156
177
  is_error: bool = False,
157
178
  truncate_content: bool = True,
158
179
  additional_message: Text | None = None,
180
+ pre_content: Text | None = None,
159
181
  ) -> None:
160
182
  """
161
183
  Unified method to display formatted messages to the console.
@@ -170,6 +192,8 @@ class ConsoleDisplay:
170
192
  max_item_length: Optional max length for bottom metadata items (with ellipsis)
171
193
  is_error: For tool results, whether this is an error (uses red color)
172
194
  truncate_content: Whether to truncate long content
195
+ additional_message: Optional Rich Text appended after the main content
196
+ pre_content: Optional Rich Text shown before the main content
173
197
  """
174
198
  # Get configuration for this message type
175
199
  config = MESSAGE_CONFIGS[message_type]
@@ -191,6 +215,8 @@ class ConsoleDisplay:
191
215
  self._create_combined_separator_status(left, right_info)
192
216
 
193
217
  # Display the content
218
+ if pre_content and pre_content.plain:
219
+ console.console.print(pre_content, markup=self._markup)
194
220
  self._display_content(
195
221
  content, truncate_content, is_error, message_type, check_markdown_markers=False
196
222
  )
@@ -513,8 +539,21 @@ class ConsoleDisplay:
513
539
 
514
540
  return formatted
515
541
 
516
- def show_tool_result(self, result: CallToolResult, name: str | None = None) -> None:
517
- """Display a tool result in the new visual style."""
542
+ def show_tool_result(
543
+ self,
544
+ result: CallToolResult,
545
+ name: str | None = None,
546
+ tool_name: str | None = None,
547
+ skybridge_config: "SkybridgeServerConfig | None" = None,
548
+ ) -> None:
549
+ """Display a tool result in the new visual style.
550
+
551
+ Args:
552
+ result: The tool result to display
553
+ name: Optional agent name
554
+ tool_name: Optional tool name for skybridge detection
555
+ skybridge_config: Optional skybridge configuration for the server
556
+ """
518
557
  if not self.config or not self.config.logger.show_tools:
519
558
  return
520
559
 
@@ -523,6 +562,20 @@ class ConsoleDisplay:
523
562
 
524
563
  # Analyze content to determine display format and status
525
564
  content = result.content
565
+ structured_content = getattr(result, "structuredContent", None)
566
+ has_structured = structured_content is not None
567
+
568
+ # Determine if this is a skybridge tool
569
+ is_skybridge_tool = False
570
+ skybridge_resource_uri = None
571
+ if has_structured and tool_name and skybridge_config:
572
+ # Check if this tool is a valid skybridge tool
573
+ for tool_cfg in skybridge_config.tools:
574
+ if tool_cfg.tool_name == tool_name and tool_cfg.is_valid:
575
+ is_skybridge_tool = True
576
+ skybridge_resource_uri = tool_cfg.resource_uri
577
+ break
578
+
526
579
  if result.isError:
527
580
  status = "ERROR"
528
581
  else:
@@ -544,15 +597,15 @@ class ConsoleDisplay:
544
597
 
545
598
  # Build transport channel info for bottom bar
546
599
  channel = getattr(result, "transport_channel", None)
547
- bottom_metadata = None
600
+ bottom_metadata_items: List[str] = []
548
601
  if channel:
549
602
  # Format channel info for bottom bar
550
603
  if channel == "post-json":
551
604
  transport_info = "HTTP (JSON-RPC)"
552
605
  elif channel == "post-sse":
553
- transport_info = "HTTP (SSE)"
606
+ transport_info = "Legacy SSE"
554
607
  elif channel == "get":
555
- transport_info = "HTTP (SSE)"
608
+ transport_info = "Legacy SSE"
556
609
  elif channel == "resumption":
557
610
  transport_info = "Resumption"
558
611
  elif channel == "stdio":
@@ -560,21 +613,144 @@ class ConsoleDisplay:
560
613
  else:
561
614
  transport_info = channel.upper()
562
615
 
563
- bottom_metadata = [transport_info]
616
+ bottom_metadata_items.append(transport_info)
617
+
618
+ elapsed = getattr(result, "transport_elapsed", None)
619
+ if isinstance(elapsed, (int, float)):
620
+ bottom_metadata_items.append(self._format_elapsed(float(elapsed)))
621
+
622
+ # Add structured content indicator if present
623
+ if has_structured:
624
+ bottom_metadata_items.append("Structured ■")
625
+
626
+ bottom_metadata = bottom_metadata_items or None
564
627
 
565
628
  # Build right info (without channel info)
566
629
  right_info = f"[dim]tool result - {status}[/dim]"
567
630
 
568
- # Display using unified method
569
- self.display_message(
570
- content=content,
571
- message_type=MessageType.TOOL_RESULT,
572
- name=name,
573
- right_info=right_info,
574
- bottom_metadata=bottom_metadata,
575
- is_error=result.isError,
576
- truncate_content=True,
577
- )
631
+ if has_structured:
632
+ # Handle structured content display manually to insert it before bottom separator
633
+ # Display main content without bottom separator
634
+ config = MESSAGE_CONFIGS[MessageType.TOOL_RESULT]
635
+ block_color = "red" if result.isError else config["block_color"]
636
+ arrow = config["arrow"]
637
+ arrow_style = config["arrow_style"]
638
+ left = f"[{block_color}]▎[/{block_color}][{arrow_style}]{arrow}[/{arrow_style}]"
639
+ if name:
640
+ left += f" [{block_color if not result.isError else 'red'}]{name}[/{block_color if not result.isError else 'red'}]"
641
+
642
+ # Top separator
643
+ self._create_combined_separator_status(left, right_info)
644
+
645
+ # Main content
646
+ self._display_content(
647
+ content, True, result.isError, MessageType.TOOL_RESULT, check_markdown_markers=False
648
+ )
649
+
650
+ # Structured content separator and display
651
+ console.console.print()
652
+ total_width = console.console.size.width
653
+
654
+ if is_skybridge_tool:
655
+ # Skybridge: magenta separator with resource URI
656
+ resource_label = (
657
+ f"skybridge resource: {skybridge_resource_uri}"
658
+ if skybridge_resource_uri
659
+ else "skybridge resource"
660
+ )
661
+ prefix = Text("─| ")
662
+ prefix.stylize("dim")
663
+ resource_text = Text(resource_label, style="magenta")
664
+ suffix = Text(" |")
665
+ suffix.stylize("dim")
666
+
667
+ separator_line = Text()
668
+ separator_line.append_text(prefix)
669
+ separator_line.append_text(resource_text)
670
+ separator_line.append_text(suffix)
671
+ remaining = total_width - separator_line.cell_len
672
+ if remaining > 0:
673
+ separator_line.append("─" * remaining, style="dim")
674
+ console.console.print(separator_line, markup=self._markup)
675
+ console.console.print()
676
+
677
+ # Display with bright syntax highlighting
678
+ import json
679
+
680
+ from rich.syntax import Syntax
681
+
682
+ json_str = json.dumps(structured_content, indent=2)
683
+ syntax_obj = Syntax(json_str, "json", theme=CODE_STYLE, background_color="default")
684
+ console.console.print(syntax_obj, markup=self._markup)
685
+ else:
686
+ # Regular tool: dim separator
687
+ prefix = Text("─| ")
688
+ prefix.stylize("dim")
689
+ label_text = Text("Structured Content", style="dim")
690
+ suffix = Text(" |")
691
+ suffix.stylize("dim")
692
+
693
+ separator_line = Text()
694
+ separator_line.append_text(prefix)
695
+ separator_line.append_text(label_text)
696
+ separator_line.append_text(suffix)
697
+ remaining = total_width - separator_line.cell_len
698
+ if remaining > 0:
699
+ separator_line.append("─" * remaining, style="dim")
700
+ console.console.print(separator_line, markup=self._markup)
701
+ console.console.print()
702
+
703
+ # Display truncated content in dim
704
+ from rich.pretty import Pretty
705
+
706
+ if self.config and self.config.logger.truncate_tools:
707
+ pretty_obj = Pretty(structured_content, max_length=10, max_string=50)
708
+ else:
709
+ pretty_obj = Pretty(structured_content)
710
+ console.console.print(pretty_obj, style="dim", markup=self._markup)
711
+
712
+ # Bottom separator with metadata
713
+ console.console.print()
714
+ if bottom_metadata:
715
+ display_items = (
716
+ self._shorten_items(bottom_metadata, 12) if True else bottom_metadata
717
+ )
718
+ prefix = Text("─| ")
719
+ prefix.stylize("dim")
720
+ suffix = Text(" |")
721
+ suffix.stylize("dim")
722
+ available = max(0, total_width - prefix.cell_len - suffix.cell_len)
723
+
724
+ metadata_text = self._format_bottom_metadata(
725
+ display_items,
726
+ None,
727
+ config["highlight_color"],
728
+ max_width=available,
729
+ )
730
+
731
+ line = Text()
732
+ line.append_text(prefix)
733
+ line.append_text(metadata_text)
734
+ line.append_text(suffix)
735
+ remaining = total_width - line.cell_len
736
+ if remaining > 0:
737
+ line.append("─" * remaining, style="dim")
738
+ console.console.print(line, markup=self._markup)
739
+ else:
740
+ console.console.print("─" * total_width, style="dim")
741
+ console.console.print()
742
+
743
+ else:
744
+ # No structured content - use standard display
745
+ self.display_message(
746
+ content=content,
747
+ message_type=MessageType.TOOL_RESULT,
748
+ name=name,
749
+ right_info=right_info,
750
+ bottom_metadata=bottom_metadata,
751
+ is_error=result.isError,
752
+ truncate_content=True,
753
+ )
578
754
 
579
755
  def show_tool_call(
580
756
  self,
@@ -638,9 +814,7 @@ class ConsoleDisplay:
638
814
  # No active prompt_toolkit session - display with rich as before
639
815
  # Combined separator and status line
640
816
  if agent_name:
641
- left = (
642
- f"[magenta]▎[/magenta][dim magenta]▶[/dim magenta] [magenta]{agent_name}[/magenta]"
643
- )
817
+ left = f"[magenta]▎[/magenta][dim magenta]▶[/dim magenta] [magenta]{agent_name}[/magenta]"
644
818
  else:
645
819
  left = "[magenta]▎[/magenta][dim magenta]▶[/dim magenta]"
646
820
 
@@ -696,6 +870,140 @@ class ConsoleDisplay:
696
870
  console.console.print(combined, markup=self._markup)
697
871
  console.console.print()
698
872
 
873
+ @staticmethod
874
+ def summarize_skybridge_configs(
875
+ configs: Mapping[str, "SkybridgeServerConfig"] | None,
876
+ ) -> Tuple[List[Dict[str, Any]], List[str]]:
877
+ """Convert raw Skybridge configs into display-friendly summary data."""
878
+ server_rows: List[Dict[str, Any]] = []
879
+ warnings: List[str] = []
880
+ warning_seen: Set[str] = set()
881
+
882
+ if not configs:
883
+ return server_rows, warnings
884
+
885
+ def add_warning(message: str) -> None:
886
+ formatted = message.strip()
887
+ if not formatted:
888
+ return
889
+ if formatted not in warning_seen:
890
+ warnings.append(formatted)
891
+ warning_seen.add(formatted)
892
+
893
+ for server_name in sorted(configs.keys()):
894
+ config = configs.get(server_name)
895
+ if not config:
896
+ continue
897
+ resources = list(config.ui_resources or [])
898
+ has_skybridge_signal = bool(
899
+ config.enabled or resources or config.tools or config.warnings
900
+ )
901
+ if not has_skybridge_signal:
902
+ continue
903
+
904
+ valid_resource_count = sum(1 for resource in resources if resource.is_skybridge)
905
+
906
+ server_rows.append(
907
+ {
908
+ "server_name": server_name,
909
+ "config": config,
910
+ "resources": resources,
911
+ "valid_resource_count": valid_resource_count,
912
+ "total_resource_count": len(resources),
913
+ "active_tools": [
914
+ {
915
+ "name": tool.display_name,
916
+ "template": str(tool.template_uri) if tool.template_uri else None,
917
+ }
918
+ for tool in config.tools
919
+ if tool.is_valid
920
+ ],
921
+ "enabled": config.enabled,
922
+ }
923
+ )
924
+
925
+ for warning in config.warnings:
926
+ message = warning.strip()
927
+ if not message:
928
+ continue
929
+ if not message.startswith(server_name):
930
+ message = f"{server_name} {message}"
931
+ add_warning(message)
932
+
933
+ return server_rows, warnings
934
+
935
+ def show_skybridge_summary(
936
+ self,
937
+ agent_name: str,
938
+ configs: Mapping[str, "SkybridgeServerConfig"] | None,
939
+ ) -> None:
940
+ """Display Skybridge availability and warnings."""
941
+ server_rows, warnings = self.summarize_skybridge_configs(configs)
942
+
943
+ if not server_rows and not warnings:
944
+ return
945
+
946
+ heading = "[dim]OpenAI Apps SDK ([/dim][cyan]skybridge[/cyan][dim]) detected:[/dim]"
947
+ console.console.print()
948
+ console.console.print(heading, markup=self._markup)
949
+
950
+ if not server_rows:
951
+ console.console.print("[dim] ● none detected[/dim]", markup=self._markup)
952
+ else:
953
+ for row in server_rows:
954
+ server_name = row["server_name"]
955
+ resource_count = row["valid_resource_count"]
956
+ total_resource_count = row["total_resource_count"]
957
+ tool_infos = row["active_tools"]
958
+ enabled = row["enabled"]
959
+
960
+ tool_count = len(tool_infos)
961
+ tool_word = "tool" if tool_count == 1 else "tools"
962
+ resource_word = (
963
+ "skybridge resource" if resource_count == 1 else "skybridge resources"
964
+ )
965
+ tool_segment = f"[cyan]{tool_count}[/cyan][dim] {tool_word}[/dim]"
966
+ resource_segment = f"[cyan]{resource_count}[/cyan][dim] {resource_word}[/dim]"
967
+ name_style = "cyan" if enabled else "yellow"
968
+ status_suffix = "" if enabled else "[dim] (issues detected)[/dim]"
969
+
970
+ console.console.print(
971
+ f"[dim] ● [/dim][{name_style}]{server_name}[/{name_style}]{status_suffix}"
972
+ f"[dim] — [/dim]{tool_segment}[dim], [/dim]{resource_segment}",
973
+ markup=self._markup,
974
+ )
975
+
976
+ for tool_info in tool_infos:
977
+ template_text = (
978
+ f"[dim] ({tool_info['template']})[/dim]" if tool_info["template"] else ""
979
+ )
980
+ console.console.print(
981
+ f"[dim] ▶ [/dim][white]{tool_info['name']}[/white]{template_text}",
982
+ markup=self._markup,
983
+ )
984
+
985
+ if tool_count == 0 and resource_count > 0:
986
+ console.console.print(
987
+ "[dim] ▶ tools not linked[/dim]",
988
+ markup=self._markup,
989
+ )
990
+ if not enabled and total_resource_count > resource_count:
991
+ invalid_count = total_resource_count - resource_count
992
+ invalid_word = "resource" if invalid_count == 1 else "resources"
993
+ console.console.print(
994
+ (
995
+ "[dim] ▶ "
996
+ f"[/dim][cyan]{invalid_count}[/cyan][dim] {invalid_word} detected with non-skybridge MIME type[/dim]"
997
+ ),
998
+ markup=self._markup,
999
+ )
1000
+
1001
+ for warning_entry in warnings:
1002
+ console.console.print(
1003
+ f"[dim red] ▶ [/dim red][red]warning[/red] [dim]{warning_entry}[/dim]",
1004
+ markup=self._markup,
1005
+ )
1006
+
699
1007
  async def show_assistant_message(
700
1008
  self,
701
1009
  message_text: Union[str, Text, "PromptMessageExtended"],
@@ -724,8 +1032,26 @@ class ConsoleDisplay:
724
1032
  # Extract text from PromptMessageExtended if needed
725
1033
  from fast_agent.types import PromptMessageExtended
726
1034
 
1035
+ pre_content: Text | None = None
1036
+
727
1037
  if isinstance(message_text, PromptMessageExtended):
728
1038
  display_text = message_text.last_text() or ""
1039
+
1040
+ channels = message_text.channels or {}
1041
+ reasoning_blocks = channels.get(REASONING) or []
1042
+ if reasoning_blocks:
1043
+ from fast_agent.mcp.helpers.content_helpers import get_text
1044
+
1045
+ reasoning_segments = []
1046
+ for block in reasoning_blocks:
1047
+ text = get_text(block)
1048
+ if text:
1049
+ reasoning_segments.append(text)
1050
+
1051
+ if reasoning_segments:
1052
+ joined = "\n".join(reasoning_segments)
1053
+ if joined.strip():
1054
+ pre_content = Text(joined, style="dim default")
729
1055
  else:
730
1056
  display_text = message_text
731
1057
 
@@ -743,6 +1069,7 @@ class ConsoleDisplay:
743
1069
  max_item_length=max_item_length,
744
1070
  truncate_content=False, # Assistant messages shouldn't be truncated
745
1071
  additional_message=additional_message,
1072
+ pre_content=pre_content,
746
1073
  )
747
1074
 
748
1075
  # Handle mermaid diagrams separately (after the main message)