fast-agent-mcp 0.1.13__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/METADATA +3 -4
  2. fast_agent_mcp-0.2.0.dist-info/RECORD +123 -0
  3. mcp_agent/__init__.py +75 -0
  4. mcp_agent/agents/agent.py +59 -371
  5. mcp_agent/agents/base_agent.py +522 -0
  6. mcp_agent/agents/workflow/__init__.py +1 -0
  7. mcp_agent/agents/workflow/chain_agent.py +173 -0
  8. mcp_agent/agents/workflow/evaluator_optimizer.py +362 -0
  9. mcp_agent/agents/workflow/orchestrator_agent.py +591 -0
  10. mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_models.py +27 -11
  11. mcp_agent/agents/workflow/parallel_agent.py +182 -0
  12. mcp_agent/agents/workflow/router_agent.py +307 -0
  13. mcp_agent/app.py +3 -1
  14. mcp_agent/cli/commands/bootstrap.py +18 -7
  15. mcp_agent/cli/commands/setup.py +12 -4
  16. mcp_agent/cli/main.py +1 -1
  17. mcp_agent/cli/terminal.py +1 -1
  18. mcp_agent/config.py +24 -35
  19. mcp_agent/context.py +3 -1
  20. mcp_agent/context_dependent.py +3 -1
  21. mcp_agent/core/agent_types.py +10 -7
  22. mcp_agent/core/direct_agent_app.py +179 -0
  23. mcp_agent/core/direct_decorators.py +443 -0
  24. mcp_agent/core/direct_factory.py +476 -0
  25. mcp_agent/core/enhanced_prompt.py +15 -20
  26. mcp_agent/core/fastagent.py +151 -337
  27. mcp_agent/core/interactive_prompt.py +424 -0
  28. mcp_agent/core/mcp_content.py +19 -11
  29. mcp_agent/core/prompt.py +6 -2
  30. mcp_agent/core/validation.py +89 -16
  31. mcp_agent/executor/decorator_registry.py +6 -2
  32. mcp_agent/executor/temporal.py +35 -11
  33. mcp_agent/executor/workflow_signal.py +8 -2
  34. mcp_agent/human_input/handler.py +3 -1
  35. mcp_agent/llm/__init__.py +2 -0
  36. mcp_agent/{workflows/llm → llm}/augmented_llm.py +131 -256
  37. mcp_agent/{workflows/llm → llm}/augmented_llm_passthrough.py +35 -107
  38. mcp_agent/llm/augmented_llm_playback.py +83 -0
  39. mcp_agent/{workflows/llm → llm}/model_factory.py +26 -8
  40. mcp_agent/llm/providers/__init__.py +8 -0
  41. mcp_agent/{workflows/llm → llm/providers}/anthropic_utils.py +5 -1
  42. mcp_agent/{workflows/llm → llm/providers}/augmented_llm_anthropic.py +37 -141
  43. mcp_agent/llm/providers/augmented_llm_deepseek.py +53 -0
  44. mcp_agent/{workflows/llm → llm/providers}/augmented_llm_openai.py +112 -148
  45. mcp_agent/{workflows/llm → llm}/providers/multipart_converter_anthropic.py +78 -35
  46. mcp_agent/{workflows/llm → llm}/providers/multipart_converter_openai.py +73 -44
  47. mcp_agent/{workflows/llm → llm}/providers/openai_multipart.py +18 -4
  48. mcp_agent/{workflows/llm → llm/providers}/openai_utils.py +3 -3
  49. mcp_agent/{workflows/llm → llm}/providers/sampling_converter_anthropic.py +3 -3
  50. mcp_agent/{workflows/llm → llm}/providers/sampling_converter_openai.py +3 -3
  51. mcp_agent/{workflows/llm → llm}/sampling_converter.py +0 -21
  52. mcp_agent/{workflows/llm → llm}/sampling_format_converter.py +16 -1
  53. mcp_agent/logging/logger.py +2 -2
  54. mcp_agent/mcp/gen_client.py +9 -3
  55. mcp_agent/mcp/interfaces.py +67 -45
  56. mcp_agent/mcp/logger_textio.py +97 -0
  57. mcp_agent/mcp/mcp_agent_client_session.py +12 -4
  58. mcp_agent/mcp/mcp_agent_server.py +3 -1
  59. mcp_agent/mcp/mcp_aggregator.py +124 -93
  60. mcp_agent/mcp/mcp_connection_manager.py +21 -7
  61. mcp_agent/mcp/prompt_message_multipart.py +59 -1
  62. mcp_agent/mcp/prompt_render.py +77 -0
  63. mcp_agent/mcp/prompt_serialization.py +20 -13
  64. mcp_agent/mcp/prompts/prompt_constants.py +18 -0
  65. mcp_agent/mcp/prompts/prompt_helpers.py +327 -0
  66. mcp_agent/mcp/prompts/prompt_load.py +15 -5
  67. mcp_agent/mcp/prompts/prompt_server.py +154 -87
  68. mcp_agent/mcp/prompts/prompt_template.py +26 -35
  69. mcp_agent/mcp/resource_utils.py +3 -1
  70. mcp_agent/mcp/sampling.py +24 -15
  71. mcp_agent/mcp_server/agent_server.py +8 -5
  72. mcp_agent/mcp_server_registry.py +22 -9
  73. mcp_agent/resources/examples/{workflows → in_dev}/agent_build.py +1 -1
  74. mcp_agent/resources/examples/{data-analysis → in_dev}/slides.py +1 -1
  75. mcp_agent/resources/examples/internal/agent.py +4 -2
  76. mcp_agent/resources/examples/internal/fastagent.config.yaml +8 -2
  77. mcp_agent/resources/examples/prompting/image_server.py +3 -1
  78. mcp_agent/resources/examples/prompting/work_with_image.py +19 -0
  79. mcp_agent/ui/console_display.py +27 -7
  80. fast_agent_mcp-0.1.13.dist-info/RECORD +0 -164
  81. mcp_agent/core/agent_app.py +0 -570
  82. mcp_agent/core/agent_utils.py +0 -69
  83. mcp_agent/core/decorators.py +0 -448
  84. mcp_agent/core/factory.py +0 -422
  85. mcp_agent/core/proxies.py +0 -278
  86. mcp_agent/core/types.py +0 -22
  87. mcp_agent/eval/__init__.py +0 -0
  88. mcp_agent/mcp/stdio.py +0 -114
  89. mcp_agent/resources/examples/data-analysis/analysis-campaign.py +0 -188
  90. mcp_agent/resources/examples/data-analysis/analysis.py +0 -65
  91. mcp_agent/resources/examples/data-analysis/fastagent.config.yaml +0 -41
  92. mcp_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -1471
  93. mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +0 -53
  94. mcp_agent/resources/examples/researcher/fastagent.config.yaml +0 -66
  95. mcp_agent/resources/examples/researcher/researcher-eval.py +0 -53
  96. mcp_agent/resources/examples/researcher/researcher-imp.py +0 -189
  97. mcp_agent/resources/examples/researcher/researcher.py +0 -39
  98. mcp_agent/resources/examples/workflows/chaining.py +0 -45
  99. mcp_agent/resources/examples/workflows/evaluator.py +0 -79
  100. mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -24
  101. mcp_agent/resources/examples/workflows/human_input.py +0 -26
  102. mcp_agent/resources/examples/workflows/orchestrator.py +0 -74
  103. mcp_agent/resources/examples/workflows/parallel.py +0 -79
  104. mcp_agent/resources/examples/workflows/router.py +0 -54
  105. mcp_agent/resources/examples/workflows/sse.py +0 -23
  106. mcp_agent/telemetry/__init__.py +0 -0
  107. mcp_agent/telemetry/usage_tracking.py +0 -19
  108. mcp_agent/workflows/__init__.py +0 -0
  109. mcp_agent/workflows/embedding/__init__.py +0 -0
  110. mcp_agent/workflows/embedding/embedding_base.py +0 -58
  111. mcp_agent/workflows/embedding/embedding_cohere.py +0 -49
  112. mcp_agent/workflows/embedding/embedding_openai.py +0 -37
  113. mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
  114. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +0 -447
  115. mcp_agent/workflows/intent_classifier/__init__.py +0 -0
  116. mcp_agent/workflows/intent_classifier/intent_classifier_base.py +0 -117
  117. mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +0 -130
  118. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +0 -41
  119. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +0 -41
  120. mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +0 -150
  121. mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +0 -60
  122. mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +0 -58
  123. mcp_agent/workflows/llm/__init__.py +0 -0
  124. mcp_agent/workflows/llm/augmented_llm_playback.py +0 -111
  125. mcp_agent/workflows/llm/providers/__init__.py +0 -8
  126. mcp_agent/workflows/orchestrator/__init__.py +0 -0
  127. mcp_agent/workflows/orchestrator/orchestrator.py +0 -535
  128. mcp_agent/workflows/parallel/__init__.py +0 -0
  129. mcp_agent/workflows/parallel/fan_in.py +0 -320
  130. mcp_agent/workflows/parallel/fan_out.py +0 -181
  131. mcp_agent/workflows/parallel/parallel_llm.py +0 -149
  132. mcp_agent/workflows/router/__init__.py +0 -0
  133. mcp_agent/workflows/router/router_base.py +0 -338
  134. mcp_agent/workflows/router/router_embedding.py +0 -226
  135. mcp_agent/workflows/router/router_embedding_cohere.py +0 -59
  136. mcp_agent/workflows/router/router_embedding_openai.py +0 -59
  137. mcp_agent/workflows/router/router_llm.py +0 -304
  138. mcp_agent/workflows/swarm/__init__.py +0 -0
  139. mcp_agent/workflows/swarm/swarm.py +0 -292
  140. mcp_agent/workflows/swarm/swarm_anthropic.py +0 -42
  141. mcp_agent/workflows/swarm/swarm_openai.py +0 -41
  142. {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/WHEEL +0 -0
  143. {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
  144. {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  145. /mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_prompts.py +0 -0
  146. /mcp_agent/{workflows/llm → llm}/memory.py +0 -0
  147. /mcp_agent/{workflows/llm → llm}/prompt_utils.py +0 -0
@@ -77,7 +77,9 @@ class MCPAggregator(ContextDependent):
77
77
  if self.connection_persistence:
78
78
  # Try to get existing connection manager from context
79
79
  if not hasattr(self.context, "_connection_manager"):
80
- self.context._connection_manager = MCPConnectionManager(self.context.server_registry)
80
+ self.context._connection_manager = MCPConnectionManager(
81
+ self.context.server_registry
82
+ )
81
83
  await self.context._connection_manager.__aenter__()
82
84
  self._persistent_connection_manager = self.context._connection_manager
83
85
 
@@ -133,7 +135,10 @@ class MCPAggregator(ContextDependent):
133
135
  if self.connection_persistence and self._persistent_connection_manager:
134
136
  try:
135
137
  # Only attempt cleanup if we own the connection manager
136
- if hasattr(self.context, "_connection_manager") and self.context._connection_manager == self._persistent_connection_manager:
138
+ if (
139
+ hasattr(self.context, "_connection_manager")
140
+ and self.context._connection_manager == self._persistent_connection_manager
141
+ ):
137
142
  logger.info("Shutting down all persistent connections...")
138
143
  await self._persistent_connection_manager.disconnect_all()
139
144
  await self._persistent_connection_manager.__aexit__(None, None, None)
@@ -202,7 +207,9 @@ class MCPAggregator(ContextDependent):
202
207
  },
203
208
  )
204
209
 
205
- await self._persistent_connection_manager.get_server(server_name, client_session_factory=MCPAgentClientSession)
210
+ await self._persistent_connection_manager.get_server(
211
+ server_name, client_session_factory=MCPAgentClientSession
212
+ )
206
213
 
207
214
  logger.info(
208
215
  f"MCP Servers initialized for agent '{self.agent_name}'",
@@ -239,11 +246,15 @@ class MCPAggregator(ContextDependent):
239
246
  prompts: List[Prompt] = []
240
247
 
241
248
  if self.connection_persistence:
242
- server_connection = await self._persistent_connection_manager.get_server(server_name, client_session_factory=MCPAgentClientSession)
249
+ server_connection = await self._persistent_connection_manager.get_server(
250
+ server_name, client_session_factory=MCPAgentClientSession
251
+ )
243
252
  tools = await fetch_tools(server_connection.session)
244
253
  prompts = await fetch_prompts(server_connection.session, server_name)
245
254
  else:
246
- async with gen_client(server_name, server_registry=self.context.server_registry) as client:
255
+ async with gen_client(
256
+ server_name, server_registry=self.context.server_registry
257
+ ) as client:
247
258
  tools = await fetch_tools(client)
248
259
  prompts = await fetch_prompts(client, server_name)
249
260
 
@@ -299,7 +310,9 @@ class MCPAggregator(ContextDependent):
299
310
  return None
300
311
 
301
312
  try:
302
- server_conn = await self._persistent_connection_manager.get_server(server_name, client_session_factory=MCPAgentClientSession)
313
+ server_conn = await self._persistent_connection_manager.get_server(
314
+ server_name, client_session_factory=MCPAgentClientSession
315
+ )
303
316
  # server_capabilities is a property, not a coroutine
304
317
  return server_conn.server_capabilities
305
318
  except Exception as e:
@@ -356,12 +369,16 @@ class MCPAggregator(ContextDependent):
356
369
  method = getattr(client, method_name)
357
370
  return await method(**method_args)
358
371
  except Exception as e:
359
- error_msg = f"Failed to {method_name} '{operation_name}' on server '{server_name}': {e}"
372
+ error_msg = (
373
+ f"Failed to {method_name} '{operation_name}' on server '{server_name}': {e}"
374
+ )
360
375
  logger.error(error_msg)
361
376
  return error_factory(error_msg) if error_factory else None
362
377
 
363
378
  if self.connection_persistence:
364
- server_connection = await self._persistent_connection_manager.get_server(server_name, client_session_factory=MCPAgentClientSession)
379
+ server_connection = await self._persistent_connection_manager.get_server(
380
+ server_name, client_session_factory=MCPAgentClientSession
381
+ )
365
382
  return await try_execute(server_connection.session)
366
383
  else:
367
384
  logger.debug(
@@ -372,7 +389,9 @@ class MCPAggregator(ContextDependent):
372
389
  "agent_name": self.agent_name,
373
390
  },
374
391
  )
375
- async with gen_client(server_name, server_registry=self.context.server_registry) as client:
392
+ async with gen_client(
393
+ server_name, server_registry=self.context.server_registry
394
+ ) as client:
376
395
  result = await try_execute(client)
377
396
  logger.debug(
378
397
  f"Closing temporary connection to server: {server_name}",
@@ -451,10 +470,14 @@ class MCPAggregator(ContextDependent):
451
470
  operation_name=local_tool_name,
452
471
  method_name="call_tool",
453
472
  method_args={"name": local_tool_name, "arguments": arguments},
454
- error_factory=lambda msg: CallToolResult(isError=True, content=[TextContent(type="text", text=msg)]),
473
+ error_factory=lambda msg: CallToolResult(
474
+ isError=True, content=[TextContent(type="text", text=msg)]
475
+ ),
455
476
  )
456
477
 
457
- async def get_prompt(self, prompt_name: str | None, arguments: dict[str, str] | None) -> GetPromptResult:
478
+ async def get_prompt(
479
+ self, prompt_name: str | None, arguments: dict[str, str] | None
480
+ ) -> GetPromptResult:
458
481
  """
459
482
  Get a prompt from a server.
460
483
 
@@ -508,7 +531,9 @@ class MCPAggregator(ContextDependent):
508
531
  # Check if any prompt in the cache has this name
509
532
  prompt_names = [prompt.name for prompt in self._prompt_cache[server_name]]
510
533
  if local_prompt_name not in prompt_names:
511
- logger.debug(f"Prompt '{local_prompt_name}' not found in cache for server '{server_name}'")
534
+ logger.debug(
535
+ f"Prompt '{local_prompt_name}' not found in cache for server '{server_name}'"
536
+ )
512
537
  return GetPromptResult(
513
538
  description=f"Prompt '{local_prompt_name}' not found on server '{server_name}'",
514
539
  messages=[],
@@ -550,7 +575,9 @@ class MCPAggregator(ContextDependent):
550
575
  potential_servers.append(s_name)
551
576
 
552
577
  if potential_servers:
553
- logger.debug(f"Found prompt '{local_prompt_name}' in cache for servers: {potential_servers}")
578
+ logger.debug(
579
+ f"Found prompt '{local_prompt_name}' in cache for servers: {potential_servers}"
580
+ )
554
581
 
555
582
  # Try each server from the cache
556
583
  for s_name in potential_servers:
@@ -576,7 +603,9 @@ class MCPAggregator(ContextDependent):
576
603
 
577
604
  # If we got a successful result with messages, return it
578
605
  if result and result.messages:
579
- logger.debug(f"Successfully retrieved prompt '{local_prompt_name}' from server '{s_name}'")
606
+ logger.debug(
607
+ f"Successfully retrieved prompt '{local_prompt_name}' from server '{s_name}'"
608
+ )
580
609
  # Add namespaced name using the actual server where found
581
610
  result.namespaced_name = f"{s_name}{SEP}{local_prompt_name}"
582
611
 
@@ -599,7 +628,9 @@ class MCPAggregator(ContextDependent):
599
628
  if capabilities and capabilities.prompts:
600
629
  supported_servers.append(s_name)
601
630
  else:
602
- logger.debug(f"Server '{s_name}' does not support prompts, skipping from fallback search")
631
+ logger.debug(
632
+ f"Server '{s_name}' does not support prompts, skipping from fallback search"
633
+ )
603
634
 
604
635
  # Try all supported servers in order
605
636
  for s_name in supported_servers:
@@ -620,7 +651,9 @@ class MCPAggregator(ContextDependent):
620
651
 
621
652
  # If we got a successful result with messages, return it
622
653
  if result and result.messages:
623
- logger.debug(f"Found prompt '{local_prompt_name}' on server '{s_name}' (not in cache)")
654
+ logger.debug(
655
+ f"Found prompt '{local_prompt_name}' on server '{s_name}' (not in cache)"
656
+ )
624
657
  # Add namespaced name using the actual server where found
625
658
  result.namespaced_name = f"{s_name}{SEP}{local_prompt_name}"
626
659
 
@@ -645,7 +678,9 @@ class MCPAggregator(ContextDependent):
645
678
  if s_name not in self._prompt_cache:
646
679
  self._prompt_cache[s_name] = []
647
680
  # Add if not already in the cache
648
- prompt_names_in_cache = [p.name for p in self._prompt_cache[s_name]]
681
+ prompt_names_in_cache = [
682
+ p.name for p in self._prompt_cache[s_name]
683
+ ]
649
684
  if local_prompt_name not in prompt_names_in_cache:
650
685
  self._prompt_cache[s_name].append(matching_prompts[0])
651
686
  except Exception:
@@ -665,103 +700,97 @@ class MCPAggregator(ContextDependent):
665
700
  messages=[],
666
701
  )
667
702
 
668
- async def list_prompts(self, server_name: str = None):
703
+ async def list_prompts(self, server_name: str = None) -> Dict[str, List[Prompt]]:
669
704
  """
670
705
  List available prompts from one or all servers.
671
706
 
672
707
  :param server_name: Optional server name to list prompts from. If not provided,
673
708
  lists prompts from all servers.
674
- :return: Dictionary mapping server names to lists of available prompts
709
+ :return: Dictionary mapping server names to lists of Prompt objects
675
710
  """
676
711
  if not self.initialized:
677
712
  await self.load_servers()
678
713
 
679
- results = {}
714
+ results: Dict[str, List[Prompt]] = {}
680
715
 
681
- # If we already have the data in cache and not requesting a specific server,
682
- # we can use the cache directly
683
- if not server_name:
716
+ # If specific server requested
717
+ if server_name:
718
+ if server_name not in self.server_names:
719
+ logger.error(f"Server '{server_name}' not found")
720
+ return results
721
+
722
+ # Check cache first
684
723
  async with self._prompt_cache_lock:
685
- if all(s_name in self._prompt_cache for s_name in self.server_names):
686
- # Return the cached prompt objects
687
- for s_name, prompt_list in self._prompt_cache.items():
688
- results[s_name] = prompt_list
689
- logger.debug("Returning cached prompts for all servers")
724
+ if server_name in self._prompt_cache:
725
+ results[server_name] = self._prompt_cache[server_name]
726
+ logger.debug(f"Returning cached prompts for server '{server_name}'")
690
727
  return results
691
728
 
692
- # If server_name is provided, only list prompts from that server
693
- if server_name:
694
- if server_name in self.server_names:
695
- # Check if we can use the cache
696
- async with self._prompt_cache_lock:
697
- if server_name in self._prompt_cache:
698
- results[server_name] = self._prompt_cache[server_name]
699
- logger.debug(f"Returning cached prompts for server '{server_name}'")
700
- return results
729
+ # Check if server supports prompts
730
+ capabilities = await self.get_capabilities(server_name)
731
+ if not capabilities or not capabilities.prompts:
732
+ logger.debug(f"Server '{server_name}' does not support prompts")
733
+ results[server_name] = []
734
+ return results
701
735
 
702
- # Check if server supports prompts
703
- capabilities = await self.get_capabilities(server_name)
704
- if not capabilities or not capabilities.prompts:
705
- logger.debug(f"Server '{server_name}' does not support prompts")
706
- results[server_name] = []
707
- return results
736
+ # Fetch from server
737
+ result = await self._execute_on_server(
738
+ server_name=server_name,
739
+ operation_type="prompts-list",
740
+ operation_name="",
741
+ method_name="list_prompts",
742
+ error_factory=lambda _: None,
743
+ )
744
+
745
+ # Get prompts from result
746
+ prompts = getattr(result, "prompts", [])
747
+
748
+ # Update cache
749
+ async with self._prompt_cache_lock:
750
+ self._prompt_cache[server_name] = prompts
751
+
752
+ results[server_name] = prompts
753
+ return results
754
+
755
+ # No specific server - check if we can use the cache for all servers
756
+ async with self._prompt_cache_lock:
757
+ if all(s_name in self._prompt_cache for s_name in self.server_names):
758
+ for s_name, prompt_list in self._prompt_cache.items():
759
+ results[s_name] = prompt_list
760
+ logger.debug("Returning cached prompts for all servers")
761
+ return results
762
+
763
+ # Identify servers that support prompts
764
+ supported_servers = []
765
+ for s_name in self.server_names:
766
+ capabilities = await self.get_capabilities(s_name)
767
+ if capabilities and capabilities.prompts:
768
+ supported_servers.append(s_name)
769
+ else:
770
+ logger.debug(f"Server '{s_name}' does not support prompts, skipping")
771
+ results[s_name] = []
708
772
 
709
- # If not in cache and server supports prompts, fetch from server
773
+ # Fetch prompts from supported servers
774
+ for s_name in supported_servers:
775
+ try:
710
776
  result = await self._execute_on_server(
711
- server_name=server_name,
777
+ server_name=s_name,
712
778
  operation_type="prompts-list",
713
779
  operation_name="",
714
780
  method_name="list_prompts",
715
- error_factory=lambda _: [],
781
+ error_factory=lambda _: None,
716
782
  )
717
783
 
718
- # Update cache with the result
719
- async with self._prompt_cache_lock:
720
- self._prompt_cache[server_name] = getattr(result, "prompts", [])
784
+ prompts = getattr(result, "prompts", [])
721
785
 
722
- results[server_name] = result
723
- else:
724
- logger.error(f"Server '{server_name}' not found")
725
- else:
726
- # We need to filter the servers that support prompts
727
- supported_servers = []
728
- for s_name in self.server_names:
729
- capabilities = await self.get_capabilities(s_name)
730
- if capabilities and capabilities.prompts:
731
- supported_servers.append(s_name)
732
- else:
733
- logger.debug(f"Server '{s_name}' does not support prompts, skipping")
734
- # Add empty list to results for this server
735
- results[s_name] = []
736
-
737
- # Process servers sequentially to ensure proper resource cleanup
738
- # This helps prevent resource leaks especially on Windows
739
- if supported_servers:
740
- server_results = []
741
- for s_name in supported_servers:
742
- try:
743
- result = await self._execute_on_server(
744
- server_name=s_name,
745
- operation_type="prompts-list",
746
- operation_name="",
747
- method_name="list_prompts",
748
- error_factory=lambda _: [],
749
- )
750
- server_results.append(result)
751
- except Exception as e:
752
- logger.debug(f"Error fetching prompts from {s_name}: {e}")
753
- server_results.append(e)
754
-
755
- for i, result in enumerate(server_results):
756
- if isinstance(result, BaseException):
757
- continue
758
-
759
- s_name = supported_servers[i]
760
- results[s_name] = result
786
+ # Update cache and results
787
+ async with self._prompt_cache_lock:
788
+ self._prompt_cache[s_name] = prompts
761
789
 
762
- # Update cache with the result
763
- async with self._prompt_cache_lock:
764
- self._prompt_cache[s_name] = getattr(result, "prompts", [])
790
+ results[s_name] = prompts
791
+ except Exception as e:
792
+ logger.debug(f"Error fetching prompts from {s_name}: {e}")
793
+ results[s_name] = []
765
794
 
766
795
  logger.debug(f"Available prompts across servers: {results}")
767
796
  return results
@@ -843,7 +872,9 @@ class MCPCompoundServer(Server):
843
872
  content=[TextContent(type="text", text=f"Error calling tool: {e}")],
844
873
  )
845
874
 
846
- async def _get_prompt(self, name: str = None, arguments: dict[str, str] = None) -> GetPromptResult:
875
+ async def _get_prompt(
876
+ self, name: str = None, arguments: dict[str, str] = None
877
+ ) -> GetPromptResult:
847
878
  """
848
879
  Get a prompt from the aggregated servers.
849
880
 
@@ -857,7 +888,7 @@ class MCPCompoundServer(Server):
857
888
  except Exception as e:
858
889
  return GetPromptResult(description=f"Error getting prompt: {e}", messages=[])
859
890
 
860
- async def _list_prompts(self, server_name: str = None) -> Dict[str, List[str]]:
891
+ async def _list_prompts(self, server_name: str = None) -> Dict[str, List[Prompt]]:
861
892
  """List available prompts from the aggregated servers."""
862
893
  try:
863
894
  return await self.aggregator.list_prompts(server_name=server_name)
@@ -19,6 +19,7 @@ from mcp.client.sse import sse_client
19
19
  from mcp.client.stdio import (
20
20
  StdioServerParameters,
21
21
  get_default_environment,
22
+ stdio_client,
22
23
  )
23
24
  from mcp.types import JSONRPCMessage, ServerCapabilities
24
25
 
@@ -27,8 +28,8 @@ from mcp_agent.context_dependent import ContextDependent
27
28
  from mcp_agent.core.exceptions import ServerInitializationError
28
29
  from mcp_agent.event_progress import ProgressAction
29
30
  from mcp_agent.logging.logger import get_logger
31
+ from mcp_agent.mcp.logger_textio import get_stderr_handler
30
32
  from mcp_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
31
- from mcp_agent.mcp.stdio import stdio_client_with_rich_stderr
32
33
 
33
34
  if TYPE_CHECKING:
34
35
  from mcp_agent.context import Context
@@ -133,7 +134,11 @@ class ServerConnection:
133
134
  Create a new session instance for this server connection.
134
135
  """
135
136
 
136
- read_timeout = timedelta(seconds=self.server_config.read_timeout_seconds) if self.server_config.read_timeout_seconds else None
137
+ read_timeout = (
138
+ timedelta(seconds=self.server_config.read_timeout_seconds)
139
+ if self.server_config.read_timeout_seconds
140
+ else None
141
+ )
137
142
 
138
143
  session = self._client_session_factory(read_stream, send_stream, read_timeout)
139
144
 
@@ -187,7 +192,9 @@ class MCPConnectionManager(ContextDependent):
187
192
  Integrates with the application context system for proper resource management.
188
193
  """
189
194
 
190
- def __init__(self, server_registry: "ServerRegistry", context: Optional["Context"] = None) -> None:
195
+ def __init__(
196
+ self, server_registry: "ServerRegistry", context: Optional["Context"] = None
197
+ ) -> None:
191
198
  super().__init__(context=context)
192
199
  self.server_registry = server_registry
193
200
  self.running_servers: Dict[str, ServerConnection] = {}
@@ -257,8 +264,11 @@ class MCPConnectionManager(ContextDependent):
257
264
  args=config.args,
258
265
  env={**get_default_environment(), **(config.env or {})},
259
266
  )
260
- # Create stdio client config with redirected stderr
261
- return stdio_client_with_rich_stderr(server_params)
267
+ # Create custom error handler to ensure all output is captured
268
+ error_handler = get_stderr_handler(server_name)
269
+ # Explicitly ensure we're using our custom logger for stderr
270
+ logger.debug(f"{server_name}: Creating stdio client with custom error handler")
271
+ return stdio_client(server_params, errlog=error_handler)
262
272
  elif config.transport == "sse":
263
273
  return sse_client(config.url)
264
274
  else:
@@ -317,13 +327,17 @@ class MCPConnectionManager(ContextDependent):
317
327
  # Check if the server is healthy after initialization
318
328
  if not server_conn.is_healthy():
319
329
  error_msg = server_conn._error_message or "Unknown error"
320
- raise ServerInitializationError(f"MCP Server: '{server_name}': Failed to initialize with error: '{error_msg}'. Check fastagent.config.yaml")
330
+ raise ServerInitializationError(
331
+ f"MCP Server: '{server_name}': Failed to initialize with error: '{error_msg}'. Check fastagent.config.yaml"
332
+ )
321
333
 
322
334
  return server_conn
323
335
 
324
336
  async def get_server_capabilities(self, server_name: str) -> ServerCapabilities | None:
325
337
  """Get the capabilities of a specific server."""
326
- server_conn = await self.get_server(server_name, client_session_factory=MCPAgentClientSession)
338
+ server_conn = await self.get_server(
339
+ server_name, client_session_factory=MCPAgentClientSession
340
+ )
327
341
  return server_conn.server_capabilities if server_conn else None
328
342
 
329
343
  async def disconnect_server(self, server_name: str) -> None:
@@ -7,10 +7,31 @@ from mcp.types import (
7
7
  PromptMessage,
8
8
  Role,
9
9
  TextContent,
10
+ TextResourceContents,
10
11
  )
11
12
  from pydantic import BaseModel
12
13
 
13
14
 
15
+ def get_text(content: Union[TextContent, ImageContent, EmbeddedResource]) -> str | None:
16
+ """
17
+ Extract text content from a content object if available.
18
+
19
+ Args:
20
+ content: A content object (TextContent, ImageContent, or EmbeddedResource)
21
+
22
+ Returns:
23
+ The text content as a string or None if not a text content
24
+ """
25
+ if isinstance(content, TextContent):
26
+ return content.text
27
+
28
+ if isinstance(content, EmbeddedResource):
29
+ if isinstance(content.resource, TextResourceContents):
30
+ return content.resource.text
31
+
32
+ return None
33
+
34
+
14
35
  class PromptMessageMultipart(BaseModel):
15
36
  """
16
37
  Extension of PromptMessage that handles multiple content parts.
@@ -50,7 +71,44 @@ class PromptMessageMultipart(BaseModel):
50
71
 
51
72
  def from_multipart(self) -> List[PromptMessage]:
52
73
  """Convert this PromptMessageMultipart to a sequence of standard PromptMessages."""
53
- return [PromptMessage(role=self.role, content=content_part) for content_part in self.content]
74
+ return [
75
+ PromptMessage(role=self.role, content=content_part) for content_part in self.content
76
+ ]
77
+
78
+ def first_text(self) -> str:
79
+ """
80
+ Get the first available text content from a message.
81
+
82
+ Args:
83
+ message: A PromptMessage or PromptMessageMultipart
84
+
85
+ Returns:
86
+ First text content or None if no text content exists
87
+ """
88
+ for content in self.content:
89
+ text = get_text(content)
90
+ if text is not None:
91
+ return text
92
+
93
+ return "<no text>"
94
+
95
+ def all_text(self) -> str:
96
+ """
97
+ Get all the text available.
98
+
99
+ Args:
100
+ message: A PromptMessage or PromptMessageMultipart
101
+
102
+ Returns:
103
+ First text content or None if no text content exists
104
+ """
105
+ result = []
106
+ for content in self.content:
107
+ text = get_text(content)
108
+ if text is not None:
109
+ result.append(text)
110
+
111
+ return "\n".join(result)
54
112
 
55
113
  @classmethod
56
114
  def parse_get_prompt_result(cls, result: GetPromptResult) -> List["PromptMessageMultipart"]:
@@ -0,0 +1,77 @@
1
+ """
2
+ Utilities for rendering PromptMessageMultipart objects for display.
3
+ """
4
+
5
+ from typing import List
6
+
7
+ from mcp.types import BlobResourceContents, TextResourceContents
8
+
9
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
10
+ from mcp_agent.mcp.prompts.prompt_helpers import (
11
+ get_resource_uri,
12
+ get_text,
13
+ is_image_content,
14
+ is_resource_content,
15
+ is_text_content,
16
+ )
17
+
18
+
19
+ def render_multipart_message(message: PromptMessageMultipart) -> str:
20
+ """
21
+ Render a multipart message for display purposes.
22
+
23
+ This function formats the message content for user-friendly display,
24
+ handling different content types appropriately.
25
+
26
+ Args:
27
+ message: A PromptMessageMultipart object to render
28
+
29
+ Returns:
30
+ A string representation of the message's content
31
+ """
32
+ rendered_parts: List[str] = []
33
+
34
+ for content in message.content:
35
+ if is_text_content(content):
36
+ # Handle text content
37
+ text_content = content # type: TextContent
38
+ rendered_parts.append(text_content.text)
39
+
40
+ elif is_image_content(content):
41
+ # Format details about the image
42
+ image_content = content # type: ImageContent
43
+ data_size = len(image_content.data) if image_content.data else 0
44
+ image_info = f"[IMAGE: {image_content.mimeType}, {data_size} bytes]"
45
+ rendered_parts.append(image_info)
46
+
47
+ elif is_resource_content(content):
48
+ # Handle embedded resources
49
+ resource = content # type: EmbeddedResource
50
+ uri = get_resource_uri(resource)
51
+
52
+ if isinstance(resource.resource, TextResourceContents):
53
+ # Handle text resources
54
+ text = resource.resource.text
55
+ text_length = len(text)
56
+ mime_type = resource.resource.mimeType
57
+
58
+ # Preview with truncation for long content
59
+ preview = text[:300] + ("..." if text_length > 300 else "")
60
+ resource_info = f"[EMBEDDED TEXT RESOURCE: {mime_type}, {uri}, {text_length} chars]\n{preview}"
61
+ rendered_parts.append(resource_info)
62
+
63
+ elif isinstance(resource.resource, BlobResourceContents):
64
+ # Handle blob resources (binary data)
65
+ blob_length = len(resource.resource.blob) if resource.resource.blob else 0
66
+ mime_type = resource.resource.mimeType
67
+
68
+ resource_info = f"[EMBEDDED BLOB RESOURCE: {mime_type}, {uri}, {blob_length} bytes]"
69
+ rendered_parts.append(resource_info)
70
+
71
+ else:
72
+ # Fallback for other content types
73
+ text = get_text(content)
74
+ if text is not None:
75
+ rendered_parts.append(text)
76
+
77
+ return "\n".join(rendered_parts)
@@ -22,6 +22,11 @@ from typing import List
22
22
  from mcp.types import EmbeddedResource, ImageContent, TextContent, TextResourceContents
23
23
 
24
24
  from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
25
+ from mcp_agent.mcp.prompts.prompt_constants import (
26
+ ASSISTANT_DELIMITER,
27
+ RESOURCE_DELIMITER,
28
+ USER_DELIMITER,
29
+ )
25
30
 
26
31
  # -------------------------------------------------------------------------
27
32
  # JSON Serialization Functions
@@ -43,7 +48,9 @@ def multipart_messages_to_json(messages: List[PromptMessageMultipart]) -> str:
43
48
  """
44
49
  # Convert to dictionaries using model_dump with proper JSON mode
45
50
  # The mode="json" parameter ensures proper handling of AnyUrl objects
46
- message_dicts = [message.model_dump(by_alias=True, mode="json", exclude_none=True) for message in messages]
51
+ message_dicts = [
52
+ message.model_dump(by_alias=True, mode="json", exclude_none=True) for message in messages
53
+ ]
47
54
 
48
55
  # Convert to JSON string
49
56
  return json.dumps(message_dicts, indent=2)
@@ -110,9 +117,9 @@ def load_messages_from_json_file(file_path: str) -> List[PromptMessageMultipart]
110
117
 
111
118
  def multipart_messages_to_delimited_format(
112
119
  messages: List[PromptMessageMultipart],
113
- user_delimiter: str = "---USER",
114
- assistant_delimiter: str = "---ASSISTANT",
115
- resource_delimiter: str = "---RESOURCE",
120
+ user_delimiter: str = USER_DELIMITER,
121
+ assistant_delimiter: str = ASSISTANT_DELIMITER,
122
+ resource_delimiter: str = RESOURCE_DELIMITER,
116
123
  combine_text: bool = True, # Set to False to maintain backward compatibility
117
124
  ) -> List[str]:
118
125
  """
@@ -189,9 +196,9 @@ def multipart_messages_to_delimited_format(
189
196
 
190
197
  def delimited_format_to_multipart_messages(
191
198
  content: str,
192
- user_delimiter: str = "---USER",
193
- assistant_delimiter: str = "---ASSISTANT",
194
- resource_delimiter: str = "---RESOURCE",
199
+ user_delimiter: str = USER_DELIMITER,
200
+ assistant_delimiter: str = ASSISTANT_DELIMITER,
201
+ resource_delimiter: str = RESOURCE_DELIMITER,
195
202
  ) -> List[PromptMessageMultipart]:
196
203
  """
197
204
  Parse hybrid delimited format into PromptMessageMultipart objects:
@@ -365,9 +372,9 @@ def delimited_format_to_multipart_messages(
365
372
  def save_messages_to_delimited_file(
366
373
  messages: List[PromptMessageMultipart],
367
374
  file_path: str,
368
- user_delimiter: str = "---USER",
369
- assistant_delimiter: str = "---ASSISTANT",
370
- resource_delimiter: str = "---RESOURCE",
375
+ user_delimiter: str = USER_DELIMITER,
376
+ assistant_delimiter: str = ASSISTANT_DELIMITER,
377
+ resource_delimiter: str = RESOURCE_DELIMITER,
371
378
  combine_text: bool = True,
372
379
  ) -> None:
373
380
  """
@@ -395,9 +402,9 @@ def save_messages_to_delimited_file(
395
402
 
396
403
  def load_messages_from_delimited_file(
397
404
  file_path: str,
398
- user_delimiter: str = "---USER",
399
- assistant_delimiter: str = "---ASSISTANT",
400
- resource_delimiter: str = "---RESOURCE",
405
+ user_delimiter: str = USER_DELIMITER,
406
+ assistant_delimiter: str = ASSISTANT_DELIMITER,
407
+ resource_delimiter: str = RESOURCE_DELIMITER,
401
408
  ) -> List[PromptMessageMultipart]:
402
409
  """
403
410
  Load PromptMessageMultipart objects from a file in hybrid delimited format.