fast-agent-mcp 0.2.35__py3-none-any.whl → 0.2.37__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

Files changed (73) hide show
  1. {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/METADATA +15 -12
  2. {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/RECORD +55 -56
  3. {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/licenses/LICENSE +1 -1
  4. mcp_agent/agents/base_agent.py +2 -2
  5. mcp_agent/agents/workflow/router_agent.py +1 -1
  6. mcp_agent/cli/commands/quickstart.py +59 -5
  7. mcp_agent/config.py +10 -0
  8. mcp_agent/context.py +1 -4
  9. mcp_agent/core/agent_types.py +7 -6
  10. mcp_agent/core/direct_decorators.py +14 -0
  11. mcp_agent/core/direct_factory.py +1 -0
  12. mcp_agent/core/enhanced_prompt.py +73 -13
  13. mcp_agent/core/fastagent.py +23 -2
  14. mcp_agent/core/interactive_prompt.py +118 -8
  15. mcp_agent/human_input/elicitation_form.py +723 -0
  16. mcp_agent/human_input/elicitation_forms.py +59 -0
  17. mcp_agent/human_input/elicitation_handler.py +88 -0
  18. mcp_agent/human_input/elicitation_state.py +34 -0
  19. mcp_agent/llm/augmented_llm.py +31 -0
  20. mcp_agent/llm/providers/augmented_llm_anthropic.py +11 -23
  21. mcp_agent/llm/providers/augmented_llm_azure.py +4 -4
  22. mcp_agent/llm/providers/augmented_llm_google_native.py +4 -2
  23. mcp_agent/llm/providers/augmented_llm_openai.py +195 -12
  24. mcp_agent/llm/providers/multipart_converter_openai.py +4 -3
  25. mcp_agent/mcp/elicitation_factory.py +84 -0
  26. mcp_agent/mcp/elicitation_handlers.py +155 -0
  27. mcp_agent/mcp/helpers/content_helpers.py +27 -0
  28. mcp_agent/mcp/helpers/server_config_helpers.py +10 -8
  29. mcp_agent/mcp/interfaces.py +1 -1
  30. mcp_agent/mcp/mcp_agent_client_session.py +44 -1
  31. mcp_agent/mcp/mcp_aggregator.py +56 -11
  32. mcp_agent/mcp/mcp_connection_manager.py +30 -18
  33. mcp_agent/mcp_server/agent_server.py +2 -0
  34. mcp_agent/mcp_server_registry.py +16 -8
  35. mcp_agent/resources/examples/data-analysis/analysis.py +1 -2
  36. mcp_agent/resources/examples/mcp/elicitations/README.md +157 -0
  37. mcp_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  38. mcp_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +232 -0
  39. mcp_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  40. mcp_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  41. mcp_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  42. mcp_agent/resources/examples/mcp/elicitations/forms_demo.py +111 -0
  43. mcp_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  44. mcp_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  45. mcp_agent/resources/examples/{prompting/agent.py → mcp/elicitations/tool_call.py} +4 -5
  46. mcp_agent/resources/examples/mcp/state-transfer/agent_two.py +1 -1
  47. mcp_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +1 -1
  48. mcp_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +1 -0
  49. mcp_agent/resources/examples/workflows/evaluator.py +1 -1
  50. mcp_agent/resources/examples/workflows/graded_report.md +89 -0
  51. mcp_agent/resources/examples/workflows/orchestrator.py +7 -9
  52. mcp_agent/resources/examples/workflows/parallel.py +0 -2
  53. mcp_agent/resources/examples/workflows/short_story.md +13 -0
  54. mcp_agent/resources/examples/in_dev/agent_build.py +0 -84
  55. mcp_agent/resources/examples/in_dev/css-LICENSE.txt +0 -21
  56. mcp_agent/resources/examples/in_dev/slides.py +0 -110
  57. mcp_agent/resources/examples/internal/agent.py +0 -20
  58. mcp_agent/resources/examples/internal/fastagent.config.yaml +0 -66
  59. mcp_agent/resources/examples/internal/history_transfer.py +0 -35
  60. mcp_agent/resources/examples/internal/job.py +0 -84
  61. mcp_agent/resources/examples/internal/prompt_category.py +0 -21
  62. mcp_agent/resources/examples/internal/prompt_sizing.py +0 -51
  63. mcp_agent/resources/examples/internal/simple.txt +0 -2
  64. mcp_agent/resources/examples/internal/sizer.py +0 -20
  65. mcp_agent/resources/examples/internal/social.py +0 -67
  66. mcp_agent/resources/examples/prompting/__init__.py +0 -3
  67. mcp_agent/resources/examples/prompting/delimited_prompt.txt +0 -14
  68. mcp_agent/resources/examples/prompting/fastagent.config.yaml +0 -43
  69. mcp_agent/resources/examples/prompting/image_server.py +0 -52
  70. mcp_agent/resources/examples/prompting/prompt1.txt +0 -6
  71. mcp_agent/resources/examples/prompting/work_with_image.py +0 -19
  72. {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/WHEEL +0 -0
  73. {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,84 @@
1
+ """
2
+ Factory for resolving elicitation handlers with proper precedence.
3
+ """
4
+
5
+ from typing import Any, Optional
6
+
7
+ from mcp.client.session import ElicitationFnT
8
+
9
+ from mcp_agent.core.agent_types import AgentConfig
10
+ from mcp_agent.logging.logger import get_logger
11
+ from mcp_agent.mcp.elicitation_handlers import (
12
+ auto_cancel_elicitation_handler,
13
+ forms_elicitation_handler,
14
+ )
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ def resolve_elicitation_handler(
20
+ agent_config: AgentConfig, app_config: Any, server_config: Any = None
21
+ ) -> Optional[ElicitationFnT]:
22
+ """Resolve elicitation handler with proper precedence.
23
+
24
+ Precedence order:
25
+ 1. Agent decorator supplied (highest precedence)
26
+ 2. Server-specific config file setting
27
+ 3. Global config file setting
28
+ 4. Default forms handler (lowest precedence)
29
+
30
+ Args:
31
+ agent_config: Agent configuration from decorator
32
+ app_config: Application configuration from YAML
33
+ server_config: Server-specific configuration (optional)
34
+
35
+ Returns:
36
+ ElicitationFnT handler or None (no elicitation capability)
37
+ """
38
+
39
+ # 1. Decorator takes highest precedence
40
+ if agent_config.elicitation_handler:
41
+ logger.debug(f"Using decorator-provided elicitation handler for agent {agent_config.name}")
42
+ return agent_config.elicitation_handler
43
+
44
+ # 2. Check server-specific config first
45
+ if server_config:
46
+ elicitation_config = getattr(server_config, "elicitation", {})
47
+ if isinstance(elicitation_config, dict):
48
+ mode = elicitation_config.get("mode")
49
+ else:
50
+ mode = getattr(elicitation_config, "mode", None)
51
+
52
+ if mode:
53
+ if mode == "none":
54
+ logger.debug(f"Elicitation disabled by server config for agent {agent_config.name}")
55
+ return None # Don't advertise elicitation capability
56
+ elif mode == "auto_cancel":
57
+ logger.debug(
58
+ f"Using auto-cancel elicitation handler (server config) for agent {agent_config.name}"
59
+ )
60
+ return auto_cancel_elicitation_handler
61
+ else: # "forms" or other
62
+ logger.debug(
63
+ f"Using forms elicitation handler (server config) for agent {agent_config.name}"
64
+ )
65
+ return forms_elicitation_handler
66
+
67
+ # 3. Check global config file
68
+ elicitation_config = getattr(app_config, "elicitation", {})
69
+ if isinstance(elicitation_config, dict):
70
+ mode = elicitation_config.get("mode", "forms")
71
+ else:
72
+ mode = getattr(elicitation_config, "mode", "forms")
73
+
74
+ if mode == "none":
75
+ logger.debug(f"Elicitation disabled by global config for agent {agent_config.name}")
76
+ return None # Don't advertise elicitation capability
77
+ elif mode == "auto_cancel":
78
+ logger.debug(
79
+ f"Using auto-cancel elicitation handler (global config) for agent {agent_config.name}"
80
+ )
81
+ return auto_cancel_elicitation_handler
82
+ else: # "forms" or default
83
+ logger.debug(f"Using default forms elicitation handler for agent {agent_config.name}")
84
+ return forms_elicitation_handler
@@ -0,0 +1,155 @@
1
+ """
2
+ Predefined elicitation handlers for different use cases.
3
+ """
4
+
5
+ import json
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from mcp.shared.context import RequestContext
9
+ from mcp.types import ElicitRequestParams, ElicitResult, ErrorData
10
+
11
+ from mcp_agent.human_input.elicitation_handler import elicitation_input_callback
12
+ from mcp_agent.human_input.types import HumanInputRequest
13
+ from mcp_agent.logging.logger import get_logger
14
+ from mcp_agent.mcp.helpers.server_config_helpers import get_server_config
15
+
16
+ if TYPE_CHECKING:
17
+ from mcp import ClientSession
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ async def auto_cancel_elicitation_handler(
23
+ context: RequestContext["ClientSession", Any],
24
+ params: ElicitRequestParams,
25
+ ) -> ElicitResult | ErrorData:
26
+ """Handler that automatically cancels all elicitation requests.
27
+
28
+ Useful for production deployments where you want to advertise elicitation
29
+ capability but automatically decline all requests.
30
+ """
31
+ logger.info(f"Auto-cancelling elicitation request: {params.message}")
32
+ return ElicitResult(action="cancel")
33
+
34
+
35
+ async def forms_elicitation_handler(
36
+ context: RequestContext["ClientSession", Any], params: ElicitRequestParams
37
+ ) -> ElicitResult:
38
+ """
39
+ Interactive forms-based elicitation handler using enhanced input handler.
40
+ """
41
+ logger.info(f"Eliciting response for params: {params}")
42
+
43
+ # Get server config for additional context
44
+ server_config = get_server_config(context)
45
+ server_name = server_config.name if server_config else "Unknown Server"
46
+ server_info = (
47
+ {"command": server_config.command} if server_config and server_config.command else None
48
+ )
49
+
50
+ # Get agent name - try multiple sources in order of preference
51
+ agent_name: str | None = None
52
+
53
+ # 1. Check if we have an MCPAgentClientSession in the context
54
+ from mcp_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
55
+ if hasattr(context, "session") and isinstance(context.session, MCPAgentClientSession):
56
+ agent_name = context.session.agent_name
57
+
58
+ # 2. If no agent name yet, use a sensible default
59
+ if not agent_name:
60
+ agent_name = "Unknown Agent"
61
+
62
+ # Create human input request
63
+ request = HumanInputRequest(
64
+ prompt=params.message,
65
+ description=f"Schema: {params.requestedSchema}" if params.requestedSchema else None,
66
+ request_id=f"elicit_{id(params)}",
67
+ metadata={
68
+ "agent_name": agent_name,
69
+ "server_name": server_name,
70
+ "elicitation": True,
71
+ "requested_schema": params.requestedSchema,
72
+ },
73
+ )
74
+
75
+ try:
76
+ # Call the enhanced elicitation handler
77
+ response = await elicitation_input_callback(
78
+ request=request,
79
+ agent_name=agent_name,
80
+ server_name=server_name,
81
+ server_info=server_info,
82
+ )
83
+
84
+ # Check for special action responses
85
+ response_data = response.response.strip()
86
+
87
+ # Handle special responses
88
+ if response_data == "__DECLINED__":
89
+ return ElicitResult(action="decline")
90
+ elif response_data == "__CANCELLED__":
91
+ return ElicitResult(action="cancel")
92
+ elif response_data == "__DISABLE_SERVER__":
93
+ # Log that user wants to disable elicitation for this server
94
+ logger.warning(f"User requested to disable elicitation for server: {server_name}")
95
+ # For now, just cancel - in a full implementation, this would update server config
96
+ return ElicitResult(action="cancel")
97
+
98
+ # Parse response based on schema if provided
99
+ if params.requestedSchema:
100
+ # Check if the response is already JSON (from our form)
101
+ try:
102
+ # Try to parse as JSON first (from schema-driven form)
103
+ content = json.loads(response_data)
104
+ # Validate that all required fields are present
105
+ required_fields = params.requestedSchema.get("required", [])
106
+ for field in required_fields:
107
+ if field not in content:
108
+ logger.warning(f"Missing required field '{field}' in elicitation response")
109
+ return ElicitResult(action="decline")
110
+ except json.JSONDecodeError:
111
+ # Not JSON, try to handle as simple text response
112
+ # This is a fallback for simple schemas or text-based responses
113
+ properties = params.requestedSchema.get("properties", {})
114
+ if len(properties) == 1:
115
+ # Single field schema - try to parse based on type
116
+ field_name = list(properties.keys())[0]
117
+ field_def = properties[field_name]
118
+ field_type = field_def.get("type")
119
+
120
+ if field_type == "boolean":
121
+ # Parse boolean values
122
+ if response_data.lower() in ["yes", "y", "true", "1"]:
123
+ content = {field_name: True}
124
+ elif response_data.lower() in ["no", "n", "false", "0"]:
125
+ content = {field_name: False}
126
+ else:
127
+ return ElicitResult(action="decline")
128
+ elif field_type == "string":
129
+ content = {field_name: response_data}
130
+ elif field_type in ["number", "integer"]:
131
+ try:
132
+ value = (
133
+ int(response_data)
134
+ if field_type == "integer"
135
+ else float(response_data)
136
+ )
137
+ content = {field_name: value}
138
+ except ValueError:
139
+ return ElicitResult(action="decline")
140
+ else:
141
+ # Unknown type, just pass as string
142
+ content = {field_name: response_data}
143
+ else:
144
+ # Multiple fields but text response - can't parse reliably
145
+ logger.warning("Text response provided for multi-field schema")
146
+ return ElicitResult(action="decline")
147
+ else:
148
+ # No schema, just return the raw response
149
+ content = {"response": response_data}
150
+
151
+ # Return the response wrapped in ElicitResult with accept action
152
+ return ElicitResult(action="accept", content=content)
153
+ except (KeyboardInterrupt, EOFError, TimeoutError):
154
+ # User cancelled or timeout
155
+ return ElicitResult(action="cancel")
@@ -11,6 +11,7 @@ from mcp.types import (
11
11
  BlobResourceContents,
12
12
  EmbeddedResource,
13
13
  ImageContent,
14
+ ReadResourceResult,
14
15
  TextContent,
15
16
  TextResourceContents,
16
17
  )
@@ -114,3 +115,29 @@ def is_resource_content(content: Union[TextContent, ImageContent, EmbeddedResour
114
115
  True if the content is EmbeddedResource, False otherwise
115
116
  """
116
117
  return isinstance(content, EmbeddedResource)
118
+
119
+
120
+ def get_resource_text(result: ReadResourceResult, index: int = 0) -> Optional[str]:
121
+ """
122
+ Extract text content from a ReadResourceResult at the specified index.
123
+
124
+ Args:
125
+ result: A ReadResourceResult from an MCP resource read operation
126
+ index: Index of the content item to extract text from (default: 0)
127
+
128
+ Returns:
129
+ The text content as a string or None if not available or not text content
130
+
131
+ Raises:
132
+ IndexError: If the index is out of bounds for the contents list
133
+ """
134
+ if index >= len(result.contents):
135
+ raise IndexError(
136
+ f"Index {index} out of bounds for contents list of length {len(result.contents)}"
137
+ )
138
+
139
+ content = result.contents[index]
140
+ if isinstance(content, TextResourceContents):
141
+ return content.text
142
+
143
+ return None
@@ -1,14 +1,12 @@
1
1
  """Helper functions for type-safe server config access."""
2
2
 
3
- from typing import TYPE_CHECKING, Optional
4
-
5
- from mcp import ClientSession
3
+ from typing import TYPE_CHECKING, Any, Optional
6
4
 
7
5
  if TYPE_CHECKING:
8
6
  from mcp_agent.config import MCPServerSettings
9
7
 
10
8
 
11
- def get_server_config(ctx: ClientSession) -> Optional["MCPServerSettings"]:
9
+ def get_server_config(ctx: Any) -> Optional["MCPServerSettings"]:
12
10
  """Extract server config from context if available.
13
11
 
14
12
  Type guard helper that safely accesses server_config with proper type checking.
@@ -16,8 +14,12 @@ def get_server_config(ctx: ClientSession) -> Optional["MCPServerSettings"]:
16
14
  # Import here to avoid circular import
17
15
  from mcp_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
18
16
 
19
- if (hasattr(ctx, "session") and
20
- isinstance(ctx.session, MCPAgentClientSession) and
21
- ctx.session.server_config):
22
- return ctx.session.server_config
17
+ # Check if ctx has a session attribute (RequestContext case)
18
+ if hasattr(ctx, "session"):
19
+ if isinstance(ctx.session, MCPAgentClientSession) and hasattr(ctx.session, 'server_config'):
20
+ return ctx.session.server_config
21
+ # Also check if ctx itself is MCPAgentClientSession (direct call case)
22
+ elif isinstance(ctx, MCPAgentClientSession) and hasattr(ctx, 'server_config'):
23
+ return ctx.server_config
24
+
23
25
  return None
@@ -21,7 +21,7 @@ from typing import (
21
21
  runtime_checkable,
22
22
  )
23
23
 
24
- from a2a_types.types import AgentCard
24
+ from a2a.types import AgentCard
25
25
  from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
26
26
  from deprecated import deprecated
27
27
  from mcp import ClientSession
@@ -13,7 +13,12 @@ from mcp.shared.session import (
13
13
  ReceiveResultT,
14
14
  SendRequestT,
15
15
  )
16
- from mcp.types import Implementation, ListRootsResult, Root, ToolListChangedNotification
16
+ from mcp.types import (
17
+ Implementation,
18
+ ListRootsResult,
19
+ Root,
20
+ ToolListChangedNotification,
21
+ )
17
22
  from pydantic import FileUrl
18
23
 
19
24
  from mcp_agent.context_dependent import ContextDependent
@@ -71,6 +76,10 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
71
76
  self.server_config: MCPServerSettings | None = kwargs.pop("server_config", None)
72
77
  # Extract agent_model if provided (for auto_sampling fallback)
73
78
  self.agent_model: str | None = kwargs.pop("agent_model", None)
79
+ # Extract agent_name if provided
80
+ self.agent_name: str | None = kwargs.pop("agent_name", None)
81
+ # Extract custom elicitation handler if provided
82
+ custom_elicitation_handler = kwargs.pop("elicitation_handler", None)
74
83
 
75
84
  # Only register callbacks if the server_config has the relevant settings
76
85
  list_roots_cb = list_roots if (self.server_config and self.server_config.roots) else None
@@ -90,12 +99,46 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
90
99
  # Auto-sampling enabled at application level
91
100
  sampling_cb = sample
92
101
 
102
+ # Use custom elicitation handler if provided, otherwise resolve using factory
103
+ if custom_elicitation_handler is not None:
104
+ elicitation_handler = custom_elicitation_handler
105
+ else:
106
+ # Try to resolve using factory
107
+ elicitation_handler = None
108
+ try:
109
+ from mcp_agent.context import get_current_context
110
+ from mcp_agent.core.agent_types import AgentConfig
111
+ from mcp_agent.mcp.elicitation_factory import resolve_elicitation_handler
112
+
113
+ context = get_current_context()
114
+ if context and context.config:
115
+ # Create a minimal agent config for the factory
116
+ agent_config = AgentConfig(
117
+ name=self.agent_name or "unknown",
118
+ model=self.agent_model or "unknown",
119
+ elicitation_handler=None, # No decorator-level handler since we're in the else block
120
+ )
121
+ elicitation_handler = resolve_elicitation_handler(
122
+ agent_config, context.config, self.server_config
123
+ )
124
+ except Exception:
125
+ # If factory resolution fails, we'll use default fallback
126
+ pass
127
+
128
+ # Fallback to forms handler only if factory resolution wasn't attempted
129
+ # If factory was attempted and returned None, respect that (means no elicitation capability)
130
+ if elicitation_handler is None and not self.server_config:
131
+ from mcp_agent.mcp.elicitation_handlers import forms_elicitation_handler
132
+
133
+ elicitation_handler = forms_elicitation_handler
134
+
93
135
  super().__init__(
94
136
  *args,
95
137
  **kwargs,
96
138
  list_roots_callback=list_roots_cb,
97
139
  sampling_callback=sampling_cb,
98
140
  client_info=fast_agent,
141
+ elicitation_callback=elicitation_handler,
99
142
  )
100
143
 
101
144
  def _should_enable_auto_sampling(self) -> bool:
@@ -217,19 +217,30 @@ class MCPAggregator(ContextDependent):
217
217
 
218
218
  # Create a wrapper to capture the parameters for the client session
219
219
  def session_factory(read_stream, write_stream, read_timeout, **kwargs):
220
- # Get agent's model if this aggregator is part of an agent
221
- agent_model = None
222
- if hasattr(self, 'config') and self.config and hasattr(self.config, 'model'):
220
+ # Get agent's model and name if this aggregator is part of an agent
221
+ agent_model: str | None = None
222
+ agent_name: str | None = None
223
+ elicitation_handler = None
224
+
225
+ # Check if this aggregator is part of an Agent (which has config)
226
+ # Import here to avoid circular dependency
227
+ from mcp_agent.agents.base_agent import BaseAgent
228
+
229
+ if isinstance(self, BaseAgent):
223
230
  agent_model = self.config.model
224
-
231
+ agent_name = self.config.name
232
+ elicitation_handler = self.config.elicitation_handler
233
+
225
234
  return MCPAgentClientSession(
226
235
  read_stream,
227
236
  write_stream,
228
237
  read_timeout,
229
238
  server_name=server_name,
230
239
  agent_model=agent_model,
240
+ agent_name=agent_name,
241
+ elicitation_handler=elicitation_handler,
231
242
  tool_list_changed_callback=self._handle_tool_list_changed,
232
- **kwargs # Pass through any additional kwargs like server_config
243
+ **kwargs, # Pass through any additional kwargs like server_config
233
244
  )
234
245
 
235
246
  await self._persistent_connection_manager.get_server(
@@ -278,19 +289,27 @@ class MCPAggregator(ContextDependent):
278
289
  else:
279
290
  # Create a factory function for the client session
280
291
  def create_session(read_stream, write_stream, read_timeout, **kwargs):
281
- # Get agent's model if this aggregator is part of an agent
282
- agent_model = None
283
- if hasattr(self, 'config') and self.config and hasattr(self.config, 'model'):
292
+ # Get agent's model and name if this aggregator is part of an agent
293
+ agent_model: str | None = None
294
+ agent_name: str | None = None
295
+
296
+ # Check if this aggregator is part of an Agent (which has config)
297
+ # Import here to avoid circular dependency
298
+ from mcp_agent.agents.base_agent import BaseAgent
299
+
300
+ if isinstance(self, BaseAgent):
284
301
  agent_model = self.config.model
285
-
302
+ agent_name = self.config.name
303
+
286
304
  return MCPAgentClientSession(
287
305
  read_stream,
288
306
  write_stream,
289
307
  read_timeout,
290
308
  server_name=server_name,
291
309
  agent_model=agent_model,
310
+ agent_name=agent_name,
292
311
  tool_list_changed_callback=self._handle_tool_list_changed,
293
- **kwargs # Pass through any additional kwargs like server_config
312
+ **kwargs, # Pass through any additional kwargs like server_config
294
313
  )
295
314
 
296
315
  async with gen_client(
@@ -812,7 +831,9 @@ class MCPAggregator(ContextDependent):
812
831
  messages=[],
813
832
  )
814
833
 
815
- async def list_prompts(self, server_name: str | None = None, agent_name: str | None = None) -> Mapping[str, List[Prompt]]:
834
+ async def list_prompts(
835
+ self, server_name: str | None = None, agent_name: str | None = None
836
+ ) -> Mapping[str, List[Prompt]]:
816
837
  """
817
838
  List available prompts from one or all servers.
818
839
 
@@ -940,11 +961,23 @@ class MCPAggregator(ContextDependent):
940
961
  if self.connection_persistence:
941
962
  # Create a factory function that will include our parameters
942
963
  def create_session(read_stream, write_stream, read_timeout):
964
+ # Get agent name if available
965
+ agent_name: str | None = None
966
+
967
+ # Import here to avoid circular dependency
968
+ from mcp_agent.agents.base_agent import BaseAgent
969
+
970
+ if isinstance(self, BaseAgent):
971
+ agent_name = self.config.name
972
+ elicitation_handler = self.config.elicitation_handler
973
+
943
974
  return MCPAgentClientSession(
944
975
  read_stream,
945
976
  write_stream,
946
977
  read_timeout,
947
978
  server_name=server_name,
979
+ agent_name=agent_name,
980
+ elicitation_handler=elicitation_handler,
948
981
  tool_list_changed_callback=self._handle_tool_list_changed,
949
982
  )
950
983
 
@@ -956,11 +989,23 @@ class MCPAggregator(ContextDependent):
956
989
  else:
957
990
  # Create a factory function for the client session
958
991
  def create_session(read_stream, write_stream, read_timeout):
992
+ # Get agent name if available
993
+ agent_name: str | None = None
994
+
995
+ # Import here to avoid circular dependency
996
+ from mcp_agent.agents.base_agent import BaseAgent
997
+
998
+ if isinstance(self, BaseAgent):
999
+ agent_name = self.config.name
1000
+ elicitation_handler = self.config.elicitation_handler
1001
+
959
1002
  return MCPAgentClientSession(
960
1003
  read_stream,
961
1004
  write_stream,
962
1005
  read_timeout,
963
1006
  server_name=server_name,
1007
+ agent_name=agent_name,
1008
+ elicitation_handler=elicitation_handler,
964
1009
  tool_list_changed_callback=self._handle_tool_list_changed,
965
1010
  )
966
1011
 
@@ -166,10 +166,7 @@ class ServerConnection:
166
166
  )
167
167
 
168
168
  session = self._client_session_factory(
169
- read_stream,
170
- send_stream,
171
- read_timeout,
172
- server_config=self.server_config
169
+ read_stream, send_stream, read_timeout, server_config=self.server_config
173
170
  )
174
171
 
175
172
  self.session = session
@@ -220,19 +217,34 @@ async def _server_lifecycle_task(server_conn: ServerConnection) -> None:
220
217
 
221
218
  if "ExceptionGroup" in type(exc).__name__ and hasattr(exc, "exceptions"):
222
219
  # Handle ExceptionGroup better by extracting the actual errors
223
- error_messages = []
224
- for subexc in exc.exceptions:
225
- if isinstance(subexc, HTTPStatusError):
226
- # Special handling for HTTP errors to make them more user-friendly
227
- error_messages.append(
228
- f"HTTP Error: {subexc.response.status_code} {subexc.response.reason_phrase} for URL: {subexc.request.url}"
229
- )
230
- else:
231
- error_messages.append(f"Error: {type(subexc).__name__}: {subexc}")
232
- if hasattr(subexc, "__cause__") and subexc.__cause__:
233
- error_messages.append(
234
- f"Caused by: {type(subexc.__cause__).__name__}: {subexc.__cause__}"
235
- )
220
+ def extract_errors(exception_group):
221
+ """Recursively extract meaningful errors from ExceptionGroups"""
222
+ messages = []
223
+ for subexc in exception_group.exceptions:
224
+ if "ExceptionGroup" in type(subexc).__name__ and hasattr(subexc, "exceptions"):
225
+ # Recursively handle nested ExceptionGroups
226
+ messages.extend(extract_errors(subexc))
227
+ elif isinstance(subexc, HTTPStatusError):
228
+ # Special handling for HTTP errors to make them more user-friendly
229
+ messages.append(
230
+ f"HTTP Error: {subexc.response.status_code} {subexc.response.reason_phrase} for URL: {subexc.request.url}"
231
+ )
232
+ else:
233
+ # Show the exception type and message, plus the root cause if available
234
+ error_msg = f"{type(subexc).__name__}: {subexc}"
235
+ messages.append(error_msg)
236
+
237
+ # If there's a root cause, show that too as it's often the most informative
238
+ if hasattr(subexc, "__cause__") and subexc.__cause__:
239
+ messages.append(
240
+ f"Caused by: {type(subexc.__cause__).__name__}: {subexc.__cause__}"
241
+ )
242
+ return messages
243
+
244
+ error_messages = extract_errors(exc)
245
+ # If we didn't extract any meaningful errors, fall back to the original exception
246
+ if not error_messages:
247
+ error_messages = [f"{type(exc).__name__}: {exc}"]
236
248
  server_conn._error_message = error_messages
237
249
  else:
238
250
  # For regular exceptions, keep the traceback but format it more cleanly
@@ -309,7 +321,7 @@ class MCPConnectionManager(ContextDependent):
309
321
  self._tg = self._task_group
310
322
  logger.info(f"Auto-created task group for server: {server_name}")
311
323
 
312
- config = self.server_registry.registry.get(server_name)
324
+ config = self.server_registry.get_server_config(server_name)
313
325
  if not config:
314
326
  raise ValueError(f"Server '{server_name}' not found in registry.")
315
327
 
@@ -65,6 +65,8 @@ class AgentMCPServer:
65
65
  @self.mcp_server.tool(
66
66
  name=f"{agent_name}_send",
67
67
  description=f"Send a message to the {agent_name} agent",
68
+ structured_output=False,
69
+ # MCP 1.10.1 turns every tool in to a structured output
68
70
  )
69
71
  async def send_message(message: str, ctx: MCPContext) -> str:
70
72
  """Send a message to the agent and return its response."""
@@ -73,7 +73,11 @@ class ServerRegistry:
73
73
  """
74
74
  if config is None:
75
75
  self.registry = self.load_registry_from_file(config_path)
76
- elif config.mcp is not None and hasattr(config.mcp, 'servers') and config.mcp.servers is not None:
76
+ elif (
77
+ config.mcp is not None
78
+ and hasattr(config.mcp, "servers")
79
+ and config.mcp.servers is not None
80
+ ):
77
81
  # Ensure config.mcp exists, has a 'servers' attribute, and it's not None
78
82
  self.registry = config.mcp.servers
79
83
  else:
@@ -95,13 +99,17 @@ class ServerRegistry:
95
99
  Raises:
96
100
  ValueError: If the configuration is invalid.
97
101
  """
98
- servers = {}
102
+ servers = {}
99
103
 
100
104
  settings = get_settings(config_path)
101
-
102
- if settings.mcp is not None and hasattr(settings.mcp, 'servers') and settings.mcp.servers is not None:
105
+
106
+ if (
107
+ settings.mcp is not None
108
+ and hasattr(settings.mcp, "servers")
109
+ and settings.mcp.servers is not None
110
+ ):
103
111
  return settings.mcp.servers
104
-
112
+
105
113
  return servers
106
114
 
107
115
  @asynccontextmanager
@@ -164,7 +172,7 @@ class ServerRegistry:
164
172
  read_stream,
165
173
  write_stream,
166
174
  read_timeout_seconds,
167
- None, # No callback for stdio
175
+ server_config=config,
168
176
  )
169
177
  async with session:
170
178
  logger.info(f"{server_name}: Connected to server using stdio transport.")
@@ -192,7 +200,7 @@ class ServerRegistry:
192
200
  read_stream,
193
201
  write_stream,
194
202
  read_timeout_seconds,
195
- None, # No callback for stdio
203
+ server_config=config,
196
204
  )
197
205
  async with session:
198
206
  logger.info(f"{server_name}: Connected to server using SSE transport.")
@@ -216,7 +224,7 @@ class ServerRegistry:
216
224
  read_stream,
217
225
  write_stream,
218
226
  read_timeout_seconds,
219
- None, # No callback for stdio
227
+ server_config=config,
220
228
  )
221
229
  async with session:
222
230
  logger.info(f"{server_name}: Connected to server using HTTP transport.")