fast-agent-mcp 0.1.12__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 (169) hide show
  1. {fast_agent_mcp-0.1.12.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 +61 -415
  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 +11 -21
  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 +15 -19
  14. mcp_agent/cli/commands/bootstrap.py +19 -38
  15. mcp_agent/cli/commands/config.py +4 -4
  16. mcp_agent/cli/commands/setup.py +7 -14
  17. mcp_agent/cli/main.py +7 -10
  18. mcp_agent/cli/terminal.py +3 -3
  19. mcp_agent/config.py +25 -40
  20. mcp_agent/context.py +12 -21
  21. mcp_agent/context_dependent.py +3 -5
  22. mcp_agent/core/agent_types.py +10 -7
  23. mcp_agent/core/direct_agent_app.py +179 -0
  24. mcp_agent/core/direct_decorators.py +443 -0
  25. mcp_agent/core/direct_factory.py +476 -0
  26. mcp_agent/core/enhanced_prompt.py +23 -55
  27. mcp_agent/core/exceptions.py +8 -8
  28. mcp_agent/core/fastagent.py +145 -371
  29. mcp_agent/core/interactive_prompt.py +424 -0
  30. mcp_agent/core/mcp_content.py +17 -17
  31. mcp_agent/core/prompt.py +6 -9
  32. mcp_agent/core/request_params.py +6 -3
  33. mcp_agent/core/validation.py +92 -18
  34. mcp_agent/executor/decorator_registry.py +9 -17
  35. mcp_agent/executor/executor.py +8 -17
  36. mcp_agent/executor/task_registry.py +2 -4
  37. mcp_agent/executor/temporal.py +19 -41
  38. mcp_agent/executor/workflow.py +3 -5
  39. mcp_agent/executor/workflow_signal.py +15 -21
  40. mcp_agent/human_input/handler.py +4 -7
  41. mcp_agent/human_input/types.py +2 -3
  42. mcp_agent/llm/__init__.py +2 -0
  43. mcp_agent/llm/augmented_llm.py +450 -0
  44. mcp_agent/llm/augmented_llm_passthrough.py +162 -0
  45. mcp_agent/llm/augmented_llm_playback.py +83 -0
  46. mcp_agent/llm/memory.py +103 -0
  47. mcp_agent/{workflows/llm → llm}/model_factory.py +22 -16
  48. mcp_agent/{workflows/llm → llm}/prompt_utils.py +1 -3
  49. mcp_agent/llm/providers/__init__.py +8 -0
  50. mcp_agent/{workflows/llm → llm/providers}/anthropic_utils.py +8 -25
  51. mcp_agent/{workflows/llm → llm/providers}/augmented_llm_anthropic.py +56 -194
  52. mcp_agent/llm/providers/augmented_llm_deepseek.py +53 -0
  53. mcp_agent/{workflows/llm → llm/providers}/augmented_llm_openai.py +99 -190
  54. mcp_agent/{workflows/llm → llm}/providers/multipart_converter_anthropic.py +72 -71
  55. mcp_agent/{workflows/llm → llm}/providers/multipart_converter_openai.py +65 -71
  56. mcp_agent/{workflows/llm → llm}/providers/openai_multipart.py +16 -44
  57. mcp_agent/{workflows/llm → llm/providers}/openai_utils.py +4 -4
  58. mcp_agent/{workflows/llm → llm}/providers/sampling_converter_anthropic.py +9 -11
  59. mcp_agent/{workflows/llm → llm}/providers/sampling_converter_openai.py +8 -12
  60. mcp_agent/{workflows/llm → llm}/sampling_converter.py +3 -31
  61. mcp_agent/llm/sampling_format_converter.py +37 -0
  62. mcp_agent/logging/events.py +1 -5
  63. mcp_agent/logging/json_serializer.py +7 -6
  64. mcp_agent/logging/listeners.py +20 -23
  65. mcp_agent/logging/logger.py +17 -19
  66. mcp_agent/logging/rich_progress.py +10 -8
  67. mcp_agent/logging/tracing.py +4 -6
  68. mcp_agent/logging/transport.py +22 -22
  69. mcp_agent/mcp/gen_client.py +1 -3
  70. mcp_agent/mcp/interfaces.py +117 -110
  71. mcp_agent/mcp/logger_textio.py +97 -0
  72. mcp_agent/mcp/mcp_agent_client_session.py +7 -7
  73. mcp_agent/mcp/mcp_agent_server.py +8 -8
  74. mcp_agent/mcp/mcp_aggregator.py +102 -143
  75. mcp_agent/mcp/mcp_connection_manager.py +20 -27
  76. mcp_agent/mcp/prompt_message_multipart.py +68 -16
  77. mcp_agent/mcp/prompt_render.py +77 -0
  78. mcp_agent/mcp/prompt_serialization.py +30 -48
  79. mcp_agent/mcp/prompts/prompt_constants.py +18 -0
  80. mcp_agent/mcp/prompts/prompt_helpers.py +327 -0
  81. mcp_agent/mcp/prompts/prompt_load.py +109 -0
  82. mcp_agent/mcp/prompts/prompt_server.py +155 -195
  83. mcp_agent/mcp/prompts/prompt_template.py +35 -66
  84. mcp_agent/mcp/resource_utils.py +7 -14
  85. mcp_agent/mcp/sampling.py +17 -17
  86. mcp_agent/mcp_server/agent_server.py +13 -17
  87. mcp_agent/mcp_server_registry.py +13 -22
  88. mcp_agent/resources/examples/{workflows → in_dev}/agent_build.py +3 -2
  89. mcp_agent/resources/examples/in_dev/slides.py +110 -0
  90. mcp_agent/resources/examples/internal/agent.py +6 -3
  91. mcp_agent/resources/examples/internal/fastagent.config.yaml +8 -2
  92. mcp_agent/resources/examples/internal/job.py +2 -1
  93. mcp_agent/resources/examples/internal/prompt_category.py +1 -1
  94. mcp_agent/resources/examples/internal/prompt_sizing.py +3 -5
  95. mcp_agent/resources/examples/internal/sizer.py +2 -1
  96. mcp_agent/resources/examples/internal/social.py +2 -1
  97. mcp_agent/resources/examples/prompting/agent.py +2 -1
  98. mcp_agent/resources/examples/prompting/image_server.py +4 -8
  99. mcp_agent/resources/examples/prompting/work_with_image.py +19 -0
  100. mcp_agent/ui/console_display.py +16 -20
  101. fast_agent_mcp-0.1.12.dist-info/RECORD +0 -161
  102. mcp_agent/core/agent_app.py +0 -646
  103. mcp_agent/core/agent_utils.py +0 -71
  104. mcp_agent/core/decorators.py +0 -455
  105. mcp_agent/core/factory.py +0 -463
  106. mcp_agent/core/proxies.py +0 -269
  107. mcp_agent/core/types.py +0 -24
  108. mcp_agent/eval/__init__.py +0 -0
  109. mcp_agent/mcp/stdio.py +0 -111
  110. mcp_agent/resources/examples/data-analysis/analysis-campaign.py +0 -188
  111. mcp_agent/resources/examples/data-analysis/analysis.py +0 -65
  112. mcp_agent/resources/examples/data-analysis/fastagent.config.yaml +0 -41
  113. mcp_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -1471
  114. mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +0 -53
  115. mcp_agent/resources/examples/researcher/fastagent.config.yaml +0 -66
  116. mcp_agent/resources/examples/researcher/researcher-eval.py +0 -53
  117. mcp_agent/resources/examples/researcher/researcher-imp.py +0 -190
  118. mcp_agent/resources/examples/researcher/researcher.py +0 -38
  119. mcp_agent/resources/examples/workflows/chaining.py +0 -44
  120. mcp_agent/resources/examples/workflows/evaluator.py +0 -78
  121. mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -24
  122. mcp_agent/resources/examples/workflows/human_input.py +0 -25
  123. mcp_agent/resources/examples/workflows/orchestrator.py +0 -73
  124. mcp_agent/resources/examples/workflows/parallel.py +0 -78
  125. mcp_agent/resources/examples/workflows/router.py +0 -53
  126. mcp_agent/resources/examples/workflows/sse.py +0 -23
  127. mcp_agent/telemetry/__init__.py +0 -0
  128. mcp_agent/telemetry/usage_tracking.py +0 -18
  129. mcp_agent/workflows/__init__.py +0 -0
  130. mcp_agent/workflows/embedding/__init__.py +0 -0
  131. mcp_agent/workflows/embedding/embedding_base.py +0 -61
  132. mcp_agent/workflows/embedding/embedding_cohere.py +0 -49
  133. mcp_agent/workflows/embedding/embedding_openai.py +0 -46
  134. mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
  135. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +0 -481
  136. mcp_agent/workflows/intent_classifier/__init__.py +0 -0
  137. mcp_agent/workflows/intent_classifier/intent_classifier_base.py +0 -120
  138. mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +0 -134
  139. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +0 -45
  140. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +0 -45
  141. mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +0 -161
  142. mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +0 -60
  143. mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +0 -60
  144. mcp_agent/workflows/llm/__init__.py +0 -0
  145. mcp_agent/workflows/llm/augmented_llm.py +0 -753
  146. mcp_agent/workflows/llm/augmented_llm_passthrough.py +0 -241
  147. mcp_agent/workflows/llm/augmented_llm_playback.py +0 -109
  148. mcp_agent/workflows/llm/providers/__init__.py +0 -8
  149. mcp_agent/workflows/llm/sampling_format_converter.py +0 -22
  150. mcp_agent/workflows/orchestrator/__init__.py +0 -0
  151. mcp_agent/workflows/orchestrator/orchestrator.py +0 -578
  152. mcp_agent/workflows/parallel/__init__.py +0 -0
  153. mcp_agent/workflows/parallel/fan_in.py +0 -350
  154. mcp_agent/workflows/parallel/fan_out.py +0 -187
  155. mcp_agent/workflows/parallel/parallel_llm.py +0 -166
  156. mcp_agent/workflows/router/__init__.py +0 -0
  157. mcp_agent/workflows/router/router_base.py +0 -368
  158. mcp_agent/workflows/router/router_embedding.py +0 -240
  159. mcp_agent/workflows/router/router_embedding_cohere.py +0 -59
  160. mcp_agent/workflows/router/router_embedding_openai.py +0 -59
  161. mcp_agent/workflows/router/router_llm.py +0 -320
  162. mcp_agent/workflows/swarm/__init__.py +0 -0
  163. mcp_agent/workflows/swarm/swarm.py +0 -320
  164. mcp_agent/workflows/swarm/swarm_anthropic.py +0 -42
  165. mcp_agent/workflows/swarm/swarm_openai.py +0 -41
  166. {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/WHEEL +0 -0
  167. {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
  168. {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  169. /mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_prompts.py +0 -0
@@ -1,31 +1,31 @@
1
1
  from asyncio import Lock, gather
2
2
  from typing import (
3
- List,
4
- Dict,
5
- Optional,
6
3
  TYPE_CHECKING,
7
4
  Any,
8
5
  Callable,
6
+ Dict,
7
+ List,
8
+ Optional,
9
9
  TypeVar,
10
10
  )
11
+
11
12
  from mcp import GetPromptResult, ReadResourceResult
12
- from pydantic import AnyUrl, BaseModel, ConfigDict
13
13
  from mcp.client.session import ClientSession
14
14
  from mcp.server.lowlevel.server import Server
15
15
  from mcp.server.stdio import stdio_server
16
16
  from mcp.types import (
17
17
  CallToolResult,
18
18
  ListToolsResult,
19
+ Prompt,
19
20
  TextContent,
20
21
  Tool,
21
- Prompt,
22
22
  )
23
+ from pydantic import AnyUrl, BaseModel, ConfigDict
23
24
 
25
+ from mcp_agent.context_dependent import ContextDependent
24
26
  from mcp_agent.event_progress import ProgressAction
25
27
  from mcp_agent.logging.logger import get_logger
26
28
  from mcp_agent.mcp.gen_client import gen_client
27
-
28
- from mcp_agent.context_dependent import ContextDependent
29
29
  from mcp_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
30
30
  from mcp_agent.mcp.mcp_connection_manager import MCPConnectionManager
31
31
 
@@ -33,9 +33,7 @@ if TYPE_CHECKING:
33
33
  from mcp_agent.context import Context
34
34
 
35
35
 
36
- logger = get_logger(
37
- __name__
38
- ) # This will be replaced per-instance when agent_name is available
36
+ logger = get_logger(__name__) # This will be replaced per-instance when agent_name is available
39
37
 
40
38
  SEP = "-"
41
39
 
@@ -99,7 +97,7 @@ class MCPAggregator(ContextDependent):
99
97
  context: Optional["Context"] = None,
100
98
  name: str = None,
101
99
  **kwargs,
102
- ):
100
+ ) -> None:
103
101
  """
104
102
  :param server_names: A list of server names to connect to.
105
103
  :param connection_persistence: Whether to maintain persistent connections to servers (default: True).
@@ -130,7 +128,7 @@ class MCPAggregator(ContextDependent):
130
128
  self._prompt_cache: Dict[str, List[Prompt]] = {}
131
129
  self._prompt_cache_lock = Lock()
132
130
 
133
- async def close(self):
131
+ async def close(self) -> None:
134
132
  """
135
133
  Close all persistent connections when the aggregator is deleted.
136
134
  """
@@ -139,14 +137,11 @@ class MCPAggregator(ContextDependent):
139
137
  # Only attempt cleanup if we own the connection manager
140
138
  if (
141
139
  hasattr(self.context, "_connection_manager")
142
- and self.context._connection_manager
143
- == self._persistent_connection_manager
140
+ and self.context._connection_manager == self._persistent_connection_manager
144
141
  ):
145
142
  logger.info("Shutting down all persistent connections...")
146
143
  await self._persistent_connection_manager.disconnect_all()
147
- await self._persistent_connection_manager.__aexit__(
148
- None, None, None
149
- )
144
+ await self._persistent_connection_manager.__aexit__(None, None, None)
150
145
  delattr(self.context, "_connection_manager")
151
146
  self.initialized = False
152
147
  except Exception as e:
@@ -185,7 +180,7 @@ class MCPAggregator(ContextDependent):
185
180
  logger.error(f"Error creating MCPAggregator: {e}")
186
181
  await instance.__aexit__(None, None, None)
187
182
 
188
- async def load_servers(self):
183
+ async def load_servers(self) -> None:
189
184
  """
190
185
  Discover tools from each server in parallel and build an index of namespaced tool names.
191
186
  Also populate the prompt cache.
@@ -232,9 +227,7 @@ class MCPAggregator(ContextDependent):
232
227
  logger.error(f"Error loading tools from server '{server_name}'", data=e)
233
228
  return []
234
229
 
235
- async def fetch_prompts(
236
- client: ClientSession, server_name: str
237
- ) -> List[Prompt]:
230
+ async def fetch_prompts(client: ClientSession, server_name: str) -> List[Prompt]:
238
231
  # Only fetch prompts if the server supports them
239
232
  capabilities = await self.get_capabilities(server_name)
240
233
  if not capabilities or not capabilities.prompts:
@@ -253,10 +246,8 @@ class MCPAggregator(ContextDependent):
253
246
  prompts: List[Prompt] = []
254
247
 
255
248
  if self.connection_persistence:
256
- server_connection = (
257
- await self._persistent_connection_manager.get_server(
258
- server_name, client_session_factory=MCPAgentClientSession
259
- )
249
+ server_connection = await self._persistent_connection_manager.get_server(
250
+ server_name, client_session_factory=MCPAgentClientSession
260
251
  )
261
252
  tools = await fetch_tools(server_connection.session)
262
253
  prompts = await fetch_prompts(server_connection.session, server_name)
@@ -378,7 +369,9 @@ class MCPAggregator(ContextDependent):
378
369
  method = getattr(client, method_name)
379
370
  return await method(**method_args)
380
371
  except Exception as e:
381
- 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
+ )
382
375
  logger.error(error_msg)
383
376
  return error_factory(error_msg) if error_factory else None
384
377
 
@@ -410,9 +403,7 @@ class MCPAggregator(ContextDependent):
410
403
  )
411
404
  return result
412
405
 
413
- async def _parse_resource_name(
414
- self, name: str, resource_type: str
415
- ) -> tuple[str, str]:
406
+ async def _parse_resource_name(self, name: str, resource_type: str) -> tuple[str, str]:
416
407
  """
417
408
  Parse a possibly namespaced resource name into server name and local resource name.
418
409
 
@@ -447,9 +438,7 @@ class MCPAggregator(ContextDependent):
447
438
 
448
439
  return server_name, local_name
449
440
 
450
- async def call_tool(
451
- self, name: str, arguments: dict | None = None
452
- ) -> CallToolResult:
441
+ async def call_tool(self, name: str, arguments: dict | None = None) -> CallToolResult:
453
442
  """
454
443
  Call a namespaced tool, e.g., 'server_name.tool_name'.
455
444
  """
@@ -487,7 +476,7 @@ class MCPAggregator(ContextDependent):
487
476
  )
488
477
 
489
478
  async def get_prompt(
490
- self, prompt_name: str = None, arguments: dict[str, str] = None
479
+ self, prompt_name: str | None, arguments: dict[str, str] | None
491
480
  ) -> GetPromptResult:
492
481
  """
493
482
  Get a prompt from a server.
@@ -540,9 +529,7 @@ class MCPAggregator(ContextDependent):
540
529
  async with self._prompt_cache_lock:
541
530
  if server_name in self._prompt_cache:
542
531
  # Check if any prompt in the cache has this name
543
- prompt_names = [
544
- prompt.name for prompt in self._prompt_cache[server_name]
545
- ]
532
+ prompt_names = [prompt.name for prompt in self._prompt_cache[server_name]]
546
533
  if local_prompt_name not in prompt_names:
547
534
  logger.debug(
548
535
  f"Prompt '{local_prompt_name}' not found in cache for server '{server_name}'"
@@ -568,9 +555,7 @@ class MCPAggregator(ContextDependent):
568
555
 
569
556
  # Add namespaced name and source server to the result
570
557
  if result and result.messages:
571
- result.namespaced_name = (
572
- namespaced_name or f"{server_name}{SEP}{local_prompt_name}"
573
- )
558
+ result.namespaced_name = namespaced_name or f"{server_name}{SEP}{local_prompt_name}"
574
559
 
575
560
  # Store the arguments in the result for display purposes
576
561
  if arguments:
@@ -599,9 +584,7 @@ class MCPAggregator(ContextDependent):
599
584
  # Check if this server supports prompts
600
585
  capabilities = await self.get_capabilities(s_name)
601
586
  if not capabilities or not capabilities.prompts:
602
- logger.debug(
603
- f"Server '{s_name}' does not support prompts, skipping"
604
- )
587
+ logger.debug(f"Server '{s_name}' does not support prompts, skipping")
605
588
  continue
606
589
 
607
590
  try:
@@ -635,9 +618,7 @@ class MCPAggregator(ContextDependent):
635
618
  except Exception as e:
636
619
  logger.debug(f"Error retrieving prompt from server '{s_name}': {e}")
637
620
  else:
638
- logger.debug(
639
- f"Prompt '{local_prompt_name}' not found in any server's cache"
640
- )
621
+ logger.debug(f"Prompt '{local_prompt_name}' not found in any server's cache")
641
622
 
642
623
  # If not in cache, perform a full search as fallback (cache might be outdated)
643
624
  # First identify servers that support prompts
@@ -691,9 +672,7 @@ class MCPAggregator(ContextDependent):
691
672
  )
692
673
 
693
674
  prompts = getattr(prompt_list_result, "prompts", [])
694
- matching_prompts = [
695
- p for p in prompts if p.name == local_prompt_name
696
- ]
675
+ matching_prompts = [p for p in prompts if p.name == local_prompt_name]
697
676
  if matching_prompts:
698
677
  async with self._prompt_cache_lock:
699
678
  if s_name not in self._prompt_cache:
@@ -703,9 +682,7 @@ class MCPAggregator(ContextDependent):
703
682
  p.name for p in self._prompt_cache[s_name]
704
683
  ]
705
684
  if local_prompt_name not in prompt_names_in_cache:
706
- self._prompt_cache[s_name].append(
707
- matching_prompts[0]
708
- )
685
+ self._prompt_cache[s_name].append(matching_prompts[0])
709
686
  except Exception:
710
687
  # Ignore errors when updating cache
711
688
  pass
@@ -723,114 +700,102 @@ class MCPAggregator(ContextDependent):
723
700
  messages=[],
724
701
  )
725
702
 
726
- async def list_prompts(self, server_name: str = None):
703
+ async def list_prompts(self, server_name: str = None) -> Dict[str, List[Prompt]]:
727
704
  """
728
705
  List available prompts from one or all servers.
729
706
 
730
707
  :param server_name: Optional server name to list prompts from. If not provided,
731
708
  lists prompts from all servers.
732
- :return: Dictionary mapping server names to lists of available prompts
709
+ :return: Dictionary mapping server names to lists of Prompt objects
733
710
  """
734
711
  if not self.initialized:
735
712
  await self.load_servers()
736
713
 
737
- results = {}
714
+ results: Dict[str, List[Prompt]] = {}
715
+
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
738
721
 
739
- # If we already have the data in cache and not requesting a specific server,
740
- # we can use the cache directly
741
- if not server_name:
722
+ # Check cache first
742
723
  async with self._prompt_cache_lock:
743
- if all(s_name in self._prompt_cache for s_name in self.server_names):
744
- # Return the cached prompt objects
745
- for s_name, prompt_list in self._prompt_cache.items():
746
- results[s_name] = prompt_list
747
- 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}'")
748
727
  return results
749
728
 
750
- # If server_name is provided, only list prompts from that server
751
- if server_name:
752
- if server_name in self.server_names:
753
- # Check if we can use the cache
754
- async with self._prompt_cache_lock:
755
- if server_name in self._prompt_cache:
756
- results[server_name] = self._prompt_cache[server_name]
757
- logger.debug(
758
- f"Returning cached prompts for server '{server_name}'"
759
- )
760
- 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
761
735
 
762
- # Check if server supports prompts
763
- capabilities = await self.get_capabilities(server_name)
764
- if not capabilities or not capabilities.prompts:
765
- logger.debug(f"Server '{server_name}' does not support prompts")
766
- results[server_name] = []
767
- 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] = []
768
772
 
769
- # 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:
770
776
  result = await self._execute_on_server(
771
- server_name=server_name,
777
+ server_name=s_name,
772
778
  operation_type="prompts-list",
773
779
  operation_name="",
774
780
  method_name="list_prompts",
775
- error_factory=lambda _: [],
781
+ error_factory=lambda _: None,
776
782
  )
777
783
 
778
- # Update cache with the result
779
- async with self._prompt_cache_lock:
780
- self._prompt_cache[server_name] = getattr(result, "prompts", [])
784
+ prompts = getattr(result, "prompts", [])
781
785
 
782
- results[server_name] = result
783
- else:
784
- logger.error(f"Server '{server_name}' not found")
785
- else:
786
- # We need to filter the servers that support prompts
787
- supported_servers = []
788
- for s_name in self.server_names:
789
- capabilities = await self.get_capabilities(s_name)
790
- if capabilities and capabilities.prompts:
791
- supported_servers.append(s_name)
792
- else:
793
- logger.debug(
794
- f"Server '{s_name}' does not support prompts, skipping"
795
- )
796
- # Add empty list to results for this server
797
- results[s_name] = []
798
-
799
- # Process servers sequentially to ensure proper resource cleanup
800
- # This helps prevent resource leaks especially on Windows
801
- if supported_servers:
802
- server_results = []
803
- for s_name in supported_servers:
804
- try:
805
- result = await self._execute_on_server(
806
- server_name=s_name,
807
- operation_type="prompts-list",
808
- operation_name="",
809
- method_name="list_prompts",
810
- error_factory=lambda _: [],
811
- )
812
- server_results.append(result)
813
- except Exception as e:
814
- logger.debug(f"Error fetching prompts from {s_name}: {e}")
815
- server_results.append(e)
816
-
817
- for i, result in enumerate(server_results):
818
- if isinstance(result, BaseException):
819
- continue
820
-
821
- s_name = supported_servers[i]
822
- results[s_name] = result
786
+ # Update cache and results
787
+ async with self._prompt_cache_lock:
788
+ self._prompt_cache[s_name] = prompts
823
789
 
824
- # Update cache with the result
825
- async with self._prompt_cache_lock:
826
- 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] = []
827
794
 
828
795
  logger.debug(f"Available prompts across servers: {results}")
829
796
  return results
830
797
 
831
- async def get_resource(
832
- self, server_name: str, resource_uri: str
833
- ) -> ReadResourceResult:
798
+ async def get_resource(self, server_name: str, resource_uri: str) -> ReadResourceResult:
834
799
  """
835
800
  Get a resource directly from an MCP server by URI.
836
801
 
@@ -881,7 +846,7 @@ class MCPCompoundServer(Server):
881
846
  A compound server (server-of-servers) that aggregates multiple MCP servers and is itself an MCP server
882
847
  """
883
848
 
884
- def __init__(self, server_names: List[str], name: str = "MCPCompoundServer"):
849
+ def __init__(self, server_names: List[str], name: str = "MCPCompoundServer") -> None:
885
850
  super().__init__(name)
886
851
  self.aggregator = MCPAggregator(server_names)
887
852
 
@@ -896,9 +861,7 @@ class MCPCompoundServer(Server):
896
861
  tools_result = await self.aggregator.list_tools()
897
862
  return tools_result.tools
898
863
 
899
- async def _call_tool(
900
- self, name: str, arguments: dict | None = None
901
- ) -> CallToolResult:
864
+ async def _call_tool(self, name: str, arguments: dict | None = None) -> CallToolResult:
902
865
  """Call a specific tool from the aggregated servers."""
903
866
  try:
904
867
  result = await self.aggregator.call_tool(name=name, arguments=arguments)
@@ -920,16 +883,12 @@ class MCPCompoundServer(Server):
920
883
  arguments: Optional dictionary of string arguments for prompt templating
921
884
  """
922
885
  try:
923
- result = await self.aggregator.get_prompt(
924
- prompt_name=name, arguments=arguments
925
- )
886
+ result = await self.aggregator.get_prompt(prompt_name=name, arguments=arguments)
926
887
  return result
927
888
  except Exception as e:
928
- return GetPromptResult(
929
- description=f"Error getting prompt: {e}", messages=[]
930
- )
889
+ return GetPromptResult(description=f"Error getting prompt: {e}", messages=[])
931
890
 
932
- 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]]:
933
892
  """List available prompts from the aggregated servers."""
934
893
  try:
935
894
  return await self.aggregator.list_prompts(server_name=server_name)
@@ -2,38 +2,38 @@
2
2
  Manages the lifecycle of multiple MCP server connections.
3
3
  """
4
4
 
5
- from datetime import timedelta
6
5
  import asyncio
6
+ from datetime import timedelta
7
7
  from typing import (
8
+ TYPE_CHECKING,
8
9
  AsyncGenerator,
9
10
  Callable,
10
11
  Dict,
11
12
  Optional,
12
- TYPE_CHECKING,
13
13
  )
14
14
 
15
- from anyio import Event, create_task_group, Lock
15
+ from anyio import Event, Lock, create_task_group
16
16
  from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
17
-
18
17
  from mcp import ClientSession
18
+ 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
- from mcp.client.sse import sse_client
24
24
  from mcp.types import JSONRPCMessage, ServerCapabilities
25
25
 
26
26
  from mcp_agent.config import MCPServerSettings
27
+ 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
- from mcp_agent.context_dependent import ContextDependent
33
33
 
34
34
  if TYPE_CHECKING:
35
- from mcp_agent.mcp_server_registry import InitHookCallable, ServerRegistry
36
35
  from mcp_agent.context import Context
36
+ from mcp_agent.mcp_server_registry import InitHookCallable, ServerRegistry
37
37
 
38
38
  logger = get_logger(__name__)
39
39
 
@@ -64,7 +64,7 @@ class ServerConnection:
64
64
  ClientSession,
65
65
  ],
66
66
  init_hook: Optional["InitHookCallable"] = None,
67
- ):
67
+ ) -> None:
68
68
  self.server_name = server_name
69
69
  self.server_config = server_config
70
70
  self.session: ClientSession | None = None
@@ -194,7 +194,7 @@ class MCPConnectionManager(ContextDependent):
194
194
 
195
195
  def __init__(
196
196
  self, server_registry: "ServerRegistry", context: Optional["Context"] = None
197
- ):
197
+ ) -> None:
198
198
  super().__init__(context=context)
199
199
  self.server_registry = server_registry
200
200
  self.running_servers: Dict[str, ServerConnection] = {}
@@ -255,9 +255,7 @@ class MCPConnectionManager(ContextDependent):
255
255
  if not config:
256
256
  raise ValueError(f"Server '{server_name}' not found in registry.")
257
257
 
258
- logger.debug(
259
- f"{server_name}: Found server configuration=", data=config.model_dump()
260
- )
258
+ logger.debug(f"{server_name}: Found server configuration=", data=config.model_dump())
261
259
 
262
260
  def transport_context_factory():
263
261
  if config.transport == "stdio":
@@ -266,8 +264,11 @@ class MCPConnectionManager(ContextDependent):
266
264
  args=config.args,
267
265
  env={**get_default_environment(), **(config.env or {})},
268
266
  )
269
- # Create stdio client config with redirected stderr
270
- 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)
271
272
  elif config.transport == "sse":
272
273
  return sse_client(config.url)
273
274
  else:
@@ -309,9 +310,7 @@ class MCPConnectionManager(ContextDependent):
309
310
 
310
311
  # If server exists but isn't healthy, remove it so we can create a new one
311
312
  if server_conn:
312
- logger.info(
313
- f"{server_name}: Server exists but is unhealthy, recreating..."
314
- )
313
+ logger.info(f"{server_name}: Server exists but is unhealthy, recreating...")
315
314
  self.running_servers.pop(server_name)
316
315
  server_conn.request_shutdown()
317
316
 
@@ -334,9 +333,7 @@ class MCPConnectionManager(ContextDependent):
334
333
 
335
334
  return server_conn
336
335
 
337
- async def get_server_capabilities(
338
- self, server_name: str
339
- ) -> ServerCapabilities | None:
336
+ async def get_server_capabilities(self, server_name: str) -> ServerCapabilities | None:
340
337
  """Get the capabilities of a specific server."""
341
338
  server_conn = await self.get_server(
342
339
  server_name, client_session_factory=MCPAgentClientSession
@@ -353,13 +350,9 @@ class MCPConnectionManager(ContextDependent):
353
350
  server_conn = self.running_servers.pop(server_name, None)
354
351
  if server_conn:
355
352
  server_conn.request_shutdown()
356
- logger.info(
357
- f"{server_name}: Shutdown signal sent (lifecycle task will exit)."
358
- )
353
+ logger.info(f"{server_name}: Shutdown signal sent (lifecycle task will exit).")
359
354
  else:
360
- logger.info(
361
- f"{server_name}: No persistent connection found. Skipping server shutdown"
362
- )
355
+ logger.info(f"{server_name}: No persistent connection found. Skipping server shutdown")
363
356
 
364
357
  async def disconnect_all(self) -> None:
365
358
  """Disconnect all servers that are running under this connection manager."""