fast-agent-mcp 0.0.12__py3-none-any.whl → 0.0.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.

@@ -3,7 +3,17 @@ Decorator-based interface for MCP Agent applications.
3
3
  Provides a simplified way to create and manage agents using decorators.
4
4
  """
5
5
 
6
- from typing import List, Optional, Dict, Callable, TypeVar, Any, Union, TypeAlias
6
+ from typing import (
7
+ List,
8
+ Optional,
9
+ Dict,
10
+ Callable,
11
+ TypeVar,
12
+ Any,
13
+ Union,
14
+ TypeAlias,
15
+ Literal,
16
+ )
7
17
  from enum import Enum
8
18
  import yaml
9
19
  import argparse
@@ -11,6 +21,9 @@ from contextlib import asynccontextmanager
11
21
 
12
22
  from mcp_agent.core.exceptions import (
13
23
  AgentConfigError,
24
+ CircularDependencyError,
25
+ ModelConfigError,
26
+ PromptExitError,
14
27
  ServerConfigError,
15
28
  ProviderKeyError,
16
29
  ServerInitializationError,
@@ -52,6 +65,7 @@ class AgentType(Enum):
52
65
  PARALLEL = "parallel"
53
66
  EVALUATOR_OPTIMIZER = "evaluator_optimizer"
54
67
  ROUTER = "router"
68
+ CHAIN = "chain"
55
69
 
56
70
 
57
71
  T = TypeVar("T") # For the wrapper classes
@@ -138,6 +152,27 @@ class RouterProxy(BaseAgentProxy):
138
152
  return f"Routed to: {top_result.result} ({top_result.confidence}): {top_result.reasoning}"
139
153
 
140
154
 
155
+ class ChainProxy(BaseAgentProxy):
156
+ """Proxy for chained agent operations"""
157
+
158
+ def __init__(
159
+ self, app: MCPApp, name: str, sequence: List[str], agent_proxies: ProxyDict
160
+ ):
161
+ super().__init__(app, name)
162
+ self._sequence = sequence
163
+ self._agent_proxies = agent_proxies
164
+
165
+ async def generate_str(self, message: str) -> str:
166
+ """Chain message through a sequence of agents"""
167
+ current_message = message
168
+
169
+ for agent_name in self._sequence:
170
+ proxy = self._agent_proxies[agent_name]
171
+ current_message = await proxy.generate_str(current_message)
172
+
173
+ return current_message
174
+
175
+
141
176
  class AgentApp:
142
177
  """Main application wrapper"""
143
178
 
@@ -175,7 +210,7 @@ class AgentApp:
175
210
 
176
211
  # Pass all available agent names for auto-completion
177
212
  available_agents = list(self._agents.keys())
178
-
213
+
179
214
  # Create agent_types dictionary mapping agent names to their types
180
215
  agent_types = {}
181
216
  for name, proxy in self._agents.items():
@@ -185,6 +220,8 @@ class AgentApp:
185
220
  agent_types[name] = "Agent"
186
221
  elif isinstance(proxy, RouterProxy):
187
222
  agent_types[name] = "Router"
223
+ elif isinstance(proxy, ChainProxy):
224
+ agent_types[name] = "Chain"
188
225
  elif isinstance(proxy, WorkflowProxy):
189
226
  # For workflow proxies, check the workflow type
190
227
  workflow = proxy._workflow
@@ -228,7 +265,7 @@ class AgentApp:
228
265
  continue
229
266
 
230
267
  if user_input.upper() == "STOP":
231
- return
268
+ return result
232
269
  if user_input == "":
233
270
  continue
234
271
 
@@ -311,6 +348,7 @@ class FastAgent(ContextDependent):
311
348
  if agent_type not in [
312
349
  AgentType.PARALLEL.value,
313
350
  AgentType.EVALUATOR_OPTIMIZER.value,
351
+ AgentType.CHAIN.value,
314
352
  ]:
315
353
  self._log_agent_load(name)
316
354
  if agent_type == AgentType.BASIC.value:
@@ -343,6 +381,10 @@ class FastAgent(ContextDependent):
343
381
  f"Expected LLMRouter instance for {name}, got {type(instance)}"
344
382
  )
345
383
  return RouterProxy(self.app, name, instance)
384
+ elif agent_type == AgentType.CHAIN.value:
385
+ # Chain proxy is directly returned from _create_agents_by_type
386
+ # No need for type checking as it's already a ChainProxy
387
+ return instance
346
388
  else:
347
389
  raise ValueError(f"Unknown agent type: {agent_type}")
348
390
 
@@ -449,6 +491,15 @@ class FastAgent(ContextDependent):
449
491
  f"Evaluator-Optimizer '{name}' references non-existent components: {', '.join(missing)}"
450
492
  )
451
493
 
494
+ elif agent_type == AgentType.CHAIN.value:
495
+ # Check that all agents in the sequence exist
496
+ sequence = agent_data.get("sequence", agent_data.get("agents", []))
497
+ missing = [a for a in sequence if a not in available_components]
498
+ if missing:
499
+ raise AgentConfigError(
500
+ f"Chain '{name}' references non-existent agents: {', '.join(missing)}"
501
+ )
502
+
452
503
  def _get_model_factory(
453
504
  self,
454
505
  model: Optional[str] = None,
@@ -633,6 +684,7 @@ class FastAgent(ContextDependent):
633
684
  use_history: bool = False,
634
685
  request_params: Optional[Dict] = None,
635
686
  human_input: bool = False,
687
+ plan_type: Literal["full", "iterative"] = "full",
636
688
  ) -> Callable:
637
689
  """
638
690
  Decorator to create and register an orchestrator.
@@ -645,6 +697,7 @@ class FastAgent(ContextDependent):
645
697
  use_history: Whether to maintain conversation history (forced false)
646
698
  request_params: Additional request parameters for the LLM
647
699
  human_input: Whether to enable human input capabilities
700
+ plan_type: Planning approach - "full" generates entire plan first, "iterative" plans one step at a time
648
701
  """
649
702
  default_instruction = """
650
703
  You are an expert planner. Given an objective task and a list of MCP servers (which are collections of tools)
@@ -666,6 +719,7 @@ class FastAgent(ContextDependent):
666
719
  use_history=use_history,
667
720
  request_params=request_params,
668
721
  human_input=human_input,
722
+ plan_type=plan_type,
669
723
  )
670
724
  return decorator
671
725
 
@@ -784,6 +838,55 @@ class FastAgent(ContextDependent):
784
838
  )
785
839
  return decorator
786
840
 
841
+ def chain(
842
+ self,
843
+ name: str = "Chain",
844
+ *,
845
+ sequence: List[str] = None,
846
+ agents: List[str] = None, # Alias for sequence
847
+ instruction: str = None,
848
+ model: str | None = None,
849
+ use_history: bool = True,
850
+ request_params: Optional[Dict] = None,
851
+ ) -> Callable:
852
+ """
853
+ Decorator to create and register a chain of agents.
854
+
855
+ Args:
856
+ name: Name of the chain
857
+ sequence: List of agent names in order of execution (preferred name)
858
+ agents: Alias for sequence (backwards compatibility)
859
+ instruction: Optional custom instruction for the chain (if none provided, will autogenerate based on sequence)
860
+ model: Model specification string (not used directly in chain)
861
+ use_history: Whether to maintain conversation history
862
+ request_params: Additional request parameters
863
+ """
864
+ # Support both parameter names
865
+ agent_sequence = sequence or agents
866
+ if agent_sequence is None:
867
+ raise ValueError("Either 'sequence' or 'agents' parameter must be provided")
868
+
869
+ # Auto-generate instruction if not provided
870
+ if instruction is None:
871
+ # We'll generate it later when we have access to the agent configs and can see servers
872
+ instruction = f"Chain of agents: {', '.join(agent_sequence)}"
873
+
874
+ decorator = self._create_decorator(
875
+ AgentType.CHAIN,
876
+ default_name="Chain",
877
+ default_instruction=instruction,
878
+ default_use_history=True,
879
+ wrapper_needed=True,
880
+ )(
881
+ name=name,
882
+ sequence=agent_sequence,
883
+ instruction=instruction,
884
+ model=model,
885
+ use_history=use_history,
886
+ request_params=request_params,
887
+ )
888
+ return decorator
889
+
787
890
  async def _create_agents_by_type(
788
891
  self,
789
892
  agent_app: MCPApp,
@@ -864,7 +967,7 @@ class FastAgent(ContextDependent):
864
967
  which can be performed by LLMs with access to the servers or agents.
865
968
  """,
866
969
  servers=[], # Planner doesn't need server access
867
- model=config.model, # Use same model as orchestrator
970
+ model=config.model,
868
971
  default_request_params=base_params,
869
972
  )
870
973
  planner_agent = Agent(
@@ -885,8 +988,10 @@ class FastAgent(ContextDependent):
885
988
  available_agents=child_agents,
886
989
  context=agent_app.context,
887
990
  request_params=planner.default_request_params, # Base params already include model settings
888
- plan_type="full", # TODO -- support iterative plan type properly
889
- verb=ProgressAction.PLANNING, # Using PLANNING instead of ORCHESTRATING
991
+ plan_type=agent_data.get(
992
+ "plan_type", "full"
993
+ ), # Get plan_type from agent_data
994
+ verb=ProgressAction.PLANNING,
890
995
  )
891
996
 
892
997
  elif agent_type == AgentType.EVALUATOR_OPTIMIZER:
@@ -941,6 +1046,43 @@ class FastAgent(ContextDependent):
941
1046
  verb=ProgressAction.ROUTING, # Set verb for progress display
942
1047
  )
943
1048
 
1049
+ elif agent_type == AgentType.CHAIN:
1050
+ # Get the sequence from either parameter
1051
+ sequence = agent_data.get("sequence", agent_data.get("agents", []))
1052
+
1053
+ # Auto-generate instruction if not provided or if it's just the default
1054
+ default_instruction = f"Chain of agents: {', '.join(sequence)}"
1055
+
1056
+ # If user provided a custom instruction, use that
1057
+ # Otherwise, generate a description based on the sequence and their servers
1058
+ if config.instruction == default_instruction:
1059
+ # Get all agent names in the sequence
1060
+ agent_names = []
1061
+ all_servers = set()
1062
+
1063
+ # Collect information about the agents and their servers
1064
+ for agent_name in sequence:
1065
+ if agent_name in active_agents:
1066
+ agent_proxy = active_agents[agent_name]
1067
+ if hasattr(agent_proxy, "_agent"):
1068
+ # For LLMAgentProxy
1069
+ agent_instance = agent_proxy._agent
1070
+ agent_names.append(agent_name)
1071
+ if hasattr(agent_instance, "server_names"):
1072
+ all_servers.update(agent_instance.server_names)
1073
+ elif hasattr(agent_proxy, "_workflow"):
1074
+ # For WorkflowProxy
1075
+ agent_names.append(agent_name)
1076
+
1077
+ # Generate a better description
1078
+ if agent_names:
1079
+ server_part = f" with access to servers: {', '.join(sorted(all_servers))}" if all_servers else ""
1080
+ config.instruction = f"Sequence of agents: {', '.join(agent_names)}{server_part}."
1081
+
1082
+ # Create a ChainProxy without needing a new instance
1083
+ # Just pass the agent proxies and sequence
1084
+ instance = ChainProxy(self.app, name, sequence, active_agents)
1085
+
944
1086
  elif agent_type == AgentType.PARALLEL:
945
1087
  # Get fan-out agents (could be basic agents or other parallels)
946
1088
  fan_out_agents = self._get_agent_instances(
@@ -1020,16 +1162,18 @@ class FastAgent(ContextDependent):
1020
1162
  agent_app, AgentType.EVALUATOR_OPTIMIZER, active_agents
1021
1163
  )
1022
1164
 
1023
- def _get_parallel_dependencies(
1024
- self, name: str, visited: set, path: set
1165
+ def _get_dependencies(
1166
+ self, name: str, visited: set, path: set, agent_type: AgentType = None
1025
1167
  ) -> List[str]:
1026
1168
  """
1027
- Get dependencies for a parallel agent in topological order.
1169
+ Get dependencies for an agent in topological order.
1170
+ Works for both Parallel and Chain workflows.
1028
1171
 
1029
1172
  Args:
1030
- name: Name of the parallel agent
1173
+ name: Name of the agent
1031
1174
  visited: Set of already visited agents
1032
1175
  path: Current path for cycle detection
1176
+ agent_type: Optional type filter (e.g., only check Parallel or Chain)
1033
1177
 
1034
1178
  Returns:
1035
1179
  List of agent names in dependency order
@@ -1039,7 +1183,7 @@ class FastAgent(ContextDependent):
1039
1183
  """
1040
1184
  if name in path:
1041
1185
  path_str = " -> ".join(path)
1042
- raise ValueError(f"Circular dependency detected: {path_str} -> {name}")
1186
+ raise CircularDependencyError(f"Path: {path_str} -> {name}")
1043
1187
 
1044
1188
  if name in visited:
1045
1189
  return []
@@ -1048,15 +1192,26 @@ class FastAgent(ContextDependent):
1048
1192
  return []
1049
1193
 
1050
1194
  config = self.agents[name]
1051
- if config["type"] != AgentType.PARALLEL.value:
1195
+
1196
+ # Skip if not the requested type (when filtering)
1197
+ if agent_type and config["type"] != agent_type.value:
1052
1198
  return []
1053
1199
 
1054
1200
  path.add(name)
1055
1201
  deps = []
1056
1202
 
1057
- # Get dependencies from fan-out agents
1058
- for fan_out in config["fan_out"]:
1059
- deps.extend(self._get_parallel_dependencies(fan_out, visited, path))
1203
+ # Handle dependencies based on agent type
1204
+ if config["type"] == AgentType.PARALLEL.value:
1205
+ # Get dependencies from fan-out agents
1206
+ for fan_out in config["fan_out"]:
1207
+ deps.extend(self._get_dependencies(fan_out, visited, path, agent_type))
1208
+ elif config["type"] == AgentType.CHAIN.value:
1209
+ # Get dependencies from sequence agents
1210
+ sequence = config.get("sequence", config.get("agents", []))
1211
+ for agent_name in sequence:
1212
+ deps.extend(
1213
+ self._get_dependencies(agent_name, visited, path, agent_type)
1214
+ )
1060
1215
 
1061
1216
  # Add this agent after its dependencies
1062
1217
  deps.append(name)
@@ -1065,54 +1220,95 @@ class FastAgent(ContextDependent):
1065
1220
 
1066
1221
  return deps
1067
1222
 
1068
- async def _create_parallel_agents(
1069
- self, agent_app: MCPApp, active_agents: ProxyDict
1223
+ def _get_parallel_dependencies(
1224
+ self, name: str, visited: set, path: set
1225
+ ) -> List[str]:
1226
+ """
1227
+ Get dependencies for a parallel agent in topological order.
1228
+ Legacy function that calls the more general _get_dependencies.
1229
+
1230
+ Args:
1231
+ name: Name of the parallel agent
1232
+ visited: Set of already visited agents
1233
+ path: Current path for cycle detection
1234
+
1235
+ Returns:
1236
+ List of agent names in dependency order
1237
+
1238
+ Raises:
1239
+ ValueError: If circular dependency detected
1240
+ """
1241
+ return self._get_dependencies(name, visited, path, AgentType.PARALLEL)
1242
+
1243
+ async def _create_agents_in_dependency_order(
1244
+ self, agent_app: MCPApp, active_agents: ProxyDict, agent_type: AgentType
1070
1245
  ) -> ProxyDict:
1071
1246
  """
1072
- Create parallel execution agents in dependency order.
1247
+ Create agents in dependency order to avoid circular references.
1248
+ Works for both Parallel and Chain workflows.
1073
1249
 
1074
1250
  Args:
1075
1251
  agent_app: The main application instance
1076
1252
  active_agents: Dictionary of already created agents/proxies
1253
+ agent_type: Type of agents to create (AgentType.PARALLEL or AgentType.CHAIN)
1077
1254
 
1078
1255
  Returns:
1079
- Dictionary of initialized parallel agents
1256
+ Dictionary of initialized agents
1080
1257
  """
1081
- parallel_agents = {}
1258
+ result_agents = {}
1082
1259
  visited = set()
1083
1260
 
1084
- # Get all parallel agents
1085
- parallel_names = [
1261
+ # Get all agents of the specified type
1262
+ agent_names = [
1086
1263
  name
1087
1264
  for name, agent_data in self.agents.items()
1088
- if agent_data["type"] == AgentType.PARALLEL.value
1265
+ if agent_data["type"] == agent_type.value
1089
1266
  ]
1090
1267
 
1091
1268
  # Create agents in dependency order
1092
- for name in parallel_names:
1269
+ for name in agent_names:
1093
1270
  # Get ordered dependencies if not already processed
1094
1271
  if name not in visited:
1095
1272
  try:
1096
- ordered_agents = self._get_parallel_dependencies(
1097
- name, visited, set()
1273
+ ordered_agents = self._get_dependencies(
1274
+ name, visited, set(), agent_type
1098
1275
  )
1099
1276
  except ValueError as e:
1100
- raise ValueError(f"Error creating parallel agent {name}: {str(e)}")
1277
+ raise ValueError(
1278
+ f"Error creating {agent_type.name.lower()} agent {name}: {str(e)}"
1279
+ )
1101
1280
 
1102
1281
  # Create each agent in order
1103
1282
  for agent_name in ordered_agents:
1104
- if agent_name not in parallel_agents:
1105
- # Create one parallel agent at a time using the generic method
1283
+ if agent_name not in result_agents:
1284
+ # Create one agent at a time using the generic method
1106
1285
  agent_result = await self._create_agents_by_type(
1107
1286
  agent_app,
1108
- AgentType.PARALLEL,
1287
+ agent_type,
1109
1288
  active_agents,
1110
1289
  agent_name=agent_name,
1111
1290
  )
1112
1291
  if agent_name in agent_result:
1113
- parallel_agents[agent_name] = agent_result[agent_name]
1292
+ result_agents[agent_name] = agent_result[agent_name]
1293
+
1294
+ return result_agents
1295
+
1296
+ async def _create_parallel_agents(
1297
+ self, agent_app: MCPApp, active_agents: ProxyDict
1298
+ ) -> ProxyDict:
1299
+ """
1300
+ Create parallel execution agents in dependency order.
1301
+
1302
+ Args:
1303
+ agent_app: The main application instance
1304
+ active_agents: Dictionary of already created agents/proxies
1114
1305
 
1115
- return parallel_agents
1306
+ Returns:
1307
+ Dictionary of initialized parallel agents
1308
+ """
1309
+ return await self._create_agents_in_dependency_order(
1310
+ agent_app, active_agents, AgentType.PARALLEL
1311
+ )
1116
1312
 
1117
1313
  async def _create_routers(
1118
1314
  self, agent_app: MCPApp, active_agents: ProxyDict
@@ -1186,12 +1382,16 @@ class FastAgent(ContextDependent):
1186
1382
  agent_app, active_agents
1187
1383
  )
1188
1384
  routers = await self._create_routers(agent_app, active_agents)
1385
+ chains = await self._create_agents_in_dependency_order(
1386
+ agent_app, active_agents, AgentType.CHAIN
1387
+ )
1189
1388
 
1190
1389
  # Merge all agents into active_agents
1191
1390
  active_agents.update(orchestrators)
1192
1391
  active_agents.update(parallel_agents)
1193
1392
  active_agents.update(evaluator_optimizers)
1194
1393
  active_agents.update(routers)
1394
+ active_agents.update(chains)
1195
1395
 
1196
1396
  # Create wrapper with all agents
1197
1397
  wrapper = AgentApp(agent_app, active_agents)
@@ -1228,10 +1428,37 @@ class FastAgent(ContextDependent):
1228
1428
  had_error = True
1229
1429
  self._handle_error(
1230
1430
  e,
1231
- "Server Startup Error",
1431
+ "MCP Server Startup Error",
1232
1432
  "There was an error starting up the MCP Server.",
1233
1433
  )
1234
1434
  raise SystemExit(1)
1435
+
1436
+ except ModelConfigError as e:
1437
+ had_error = True
1438
+ self._handle_error(
1439
+ e,
1440
+ "Model Configuration Error",
1441
+ "Common models: gpt-4o, o3-mini, sonnet, haiku. for o3, set reasoning effort with o3-mini.high",
1442
+ )
1443
+ raise SystemExit(1)
1444
+
1445
+ except CircularDependencyError as e:
1446
+ had_error = True
1447
+ self._handle_error(
1448
+ e,
1449
+ "Circular Dependency Detected",
1450
+ "Check your agent configuration for circular dependencies.",
1451
+ )
1452
+ raise SystemExit(1)
1453
+
1454
+ except PromptExitError as e:
1455
+ had_error = True
1456
+ self._handle_error(
1457
+ e,
1458
+ "User requested exit",
1459
+ )
1460
+ raise SystemExit(1)
1461
+
1235
1462
  finally:
1236
1463
  # Clean up any active agents without re-raising errors
1237
1464
  if active_agents and not had_error:
@@ -1239,7 +1466,8 @@ class FastAgent(ContextDependent):
1239
1466
  if isinstance(proxy, LLMAgentProxy):
1240
1467
  try:
1241
1468
  await proxy._agent.__aexit__(None, None, None)
1242
- except Exception:
1469
+ except Exception as e:
1470
+ print(f"DEBUG {e.message}")
1243
1471
  pass # Ignore cleanup errors
1244
1472
 
1245
1473
  def _handle_error(
@@ -1,6 +1,5 @@
1
1
  import asyncio
2
2
  from rich.panel import Panel
3
- from rich.prompt import Prompt
4
3
 
5
4
  from mcp_agent.console import console
6
5
  from mcp_agent.human_input.types import (
@@ -8,10 +7,11 @@ from mcp_agent.human_input.types import (
8
7
  HumanInputResponse,
9
8
  )
10
9
  from mcp_agent.progress_display import progress_display
10
+ from mcp_agent.core.enhanced_prompt import get_enhanced_input, handle_special_commands
11
11
 
12
12
 
13
13
  async def console_input_callback(request: HumanInputRequest) -> HumanInputResponse:
14
- """Request input from a human user via console using rich panel and prompt."""
14
+ """Request input from a human user via console using prompt_toolkit."""
15
15
 
16
16
  # Prepare the prompt text
17
17
  prompt_text = request.prompt
@@ -28,26 +28,51 @@ async def console_input_callback(request: HumanInputRequest) -> HumanInputRespon
28
28
  padding=(1, 2),
29
29
  )
30
30
 
31
+ # Extract agent name from metadata dictionary
32
+ agent_name = (
33
+ request.metadata.get("agent_name", "Unknown Agent")
34
+ if request.metadata
35
+ else "Unknown Agent"
36
+ )
37
+
31
38
  # Use the context manager to pause the progress display while getting input
32
39
  with progress_display.paused():
33
40
  console.print(panel)
34
41
 
35
- if request.timeout_seconds:
36
- try:
37
- loop = asyncio.get_event_loop()
38
- response = await asyncio.wait_for(
39
- loop.run_in_executor(
40
- None, lambda: Prompt.ask("Provide your response ")
41
- ),
42
- request.timeout_seconds,
42
+ try:
43
+ if request.timeout_seconds:
44
+ try:
45
+ # Use get_enhanced_input with empty agent list to disable agent switching
46
+ response = await asyncio.wait_for(
47
+ get_enhanced_input(
48
+ agent_name=agent_name,
49
+ available_agent_names=[], # No agents for selection
50
+ show_stop_hint=False,
51
+ is_human_input=True,
52
+ toolbar_color="ansimagenta",
53
+ ),
54
+ request.timeout_seconds,
55
+ )
56
+ except asyncio.TimeoutError:
57
+ console.print("\n[red]Timeout waiting for input[/red]")
58
+ raise TimeoutError("No response received within timeout period")
59
+ else:
60
+ response = await get_enhanced_input(
61
+ agent_name=agent_name,
62
+ available_agent_names=[], # No agents for selection
63
+ show_stop_hint=False,
64
+ is_human_input=True,
65
+ toolbar_color="ansimagenta",
43
66
  )
44
- except asyncio.TimeoutError:
45
- console.print("\n[red]Timeout waiting for input[/red]")
46
- raise TimeoutError("No response received within timeout period")
47
- else:
48
- loop = asyncio.get_event_loop()
49
- response = await loop.run_in_executor(
50
- None, lambda: Prompt.ask("Provide your response ")
51
- )
67
+
68
+ # if response and (response.startswith("/") or response.startswith("@")):
69
+ await handle_special_commands(response)
70
+
71
+ except KeyboardInterrupt:
72
+ console.print("\n[yellow]Input interrupted[/yellow]")
73
+ response = ""
74
+ except EOFError:
75
+ console.print("\n[yellow]Input terminated[/yellow]")
76
+ response = ""
52
77
 
53
78
  return HumanInputResponse(request_id=request.request_id, response=response.strip())
@@ -75,15 +75,15 @@ class ServerConnection:
75
75
 
76
76
  # Signal we want to shut down
77
77
  self._shutdown_event = Event()
78
-
78
+
79
79
  # Track error state
80
80
  self._error_occurred = False
81
81
  self._error_message = None
82
-
82
+
83
83
  def is_healthy(self) -> bool:
84
84
  """Check if the server connection is healthy and ready to use."""
85
85
  return self.session is not None and not self._error_occurred
86
-
86
+
87
87
  def reset_error_state(self) -> None:
88
88
  """Reset the error state, allowing reconnection attempts."""
89
89
  self._error_occurred = False
@@ -216,10 +216,10 @@ class MCPConnectionManager(ContextDependent):
216
216
  try:
217
217
  # First request all servers to shutdown
218
218
  await self.disconnect_all()
219
-
219
+
220
220
  # Add a small delay to allow for clean shutdown
221
221
  await asyncio.sleep(0.5)
222
-
222
+
223
223
  # Then close the task group if it's active
224
224
  if self._task_group_active:
225
225
  await self._task_group.__aexit__(exc_type, exc_val, exc_tb)
@@ -305,10 +305,12 @@ class MCPConnectionManager(ContextDependent):
305
305
  server_conn = self.running_servers.get(server_name)
306
306
  if server_conn and server_conn.is_healthy():
307
307
  return server_conn
308
-
308
+
309
309
  # If server exists but isn't healthy, remove it so we can create a new one
310
310
  if server_conn:
311
- logger.info(f"{server_name}: Server exists but is unhealthy, recreating...")
311
+ logger.info(
312
+ f"{server_name}: Server exists but is unhealthy, recreating..."
313
+ )
312
314
  self.running_servers.pop(server_name)
313
315
  server_conn.request_shutdown()
314
316
 
@@ -326,9 +328,9 @@ class MCPConnectionManager(ContextDependent):
326
328
  if not server_conn.is_healthy():
327
329
  error_msg = server_conn._error_message or "Unknown error"
328
330
  raise ServerInitializationError(
329
- f"{server_name}: Failed to initialize server: {error_msg}"
331
+ f"MCP Server: '{server_name}': Failed to initialize with error: '{error_msg}'. Check fastagent.config.yaml"
330
332
  )
331
-
333
+
332
334
  return server_conn
333
335
 
334
336
  async def disconnect_server(self, server_name: str) -> None:
@@ -353,16 +355,16 @@ class MCPConnectionManager(ContextDependent):
353
355
  """Disconnect all servers that are running under this connection manager."""
354
356
  # Get a copy of servers to shutdown
355
357
  servers_to_shutdown = []
356
-
358
+
357
359
  async with self._lock:
358
360
  if not self.running_servers:
359
361
  return
360
-
362
+
361
363
  # Make a copy of the servers to shut down
362
364
  servers_to_shutdown = list(self.running_servers.items())
363
365
  # Clear the dict immediately to prevent any new access
364
366
  self.running_servers.clear()
365
-
367
+
366
368
  # Release the lock before waiting for servers to shut down
367
369
  for name, conn in servers_to_shutdown:
368
370
  logger.info(f"{name}: Requesting shutdown...")