fast-agent-mcp 0.2.36__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 (64) hide show
  1. {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.37.dist-info}/METADATA +10 -7
  2. {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.37.dist-info}/RECORD +46 -47
  3. {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.37.dist-info}/licenses/LICENSE +1 -1
  4. mcp_agent/cli/commands/quickstart.py +59 -5
  5. mcp_agent/config.py +10 -0
  6. mcp_agent/context.py +1 -4
  7. mcp_agent/core/agent_types.py +7 -6
  8. mcp_agent/core/direct_decorators.py +14 -0
  9. mcp_agent/core/direct_factory.py +1 -0
  10. mcp_agent/core/fastagent.py +23 -2
  11. mcp_agent/human_input/elicitation_form.py +723 -0
  12. mcp_agent/human_input/elicitation_forms.py +59 -0
  13. mcp_agent/human_input/elicitation_handler.py +88 -0
  14. mcp_agent/human_input/elicitation_state.py +34 -0
  15. mcp_agent/llm/providers/augmented_llm_google_native.py +4 -2
  16. mcp_agent/llm/providers/augmented_llm_openai.py +1 -1
  17. mcp_agent/mcp/elicitation_factory.py +84 -0
  18. mcp_agent/mcp/elicitation_handlers.py +155 -0
  19. mcp_agent/mcp/helpers/content_helpers.py +27 -0
  20. mcp_agent/mcp/helpers/server_config_helpers.py +10 -8
  21. mcp_agent/mcp/mcp_agent_client_session.py +44 -1
  22. mcp_agent/mcp/mcp_aggregator.py +56 -11
  23. mcp_agent/mcp/mcp_connection_manager.py +30 -18
  24. mcp_agent/mcp_server/agent_server.py +2 -0
  25. mcp_agent/mcp_server_registry.py +16 -8
  26. mcp_agent/resources/examples/data-analysis/analysis.py +1 -2
  27. mcp_agent/resources/examples/mcp/elicitations/README.md +157 -0
  28. mcp_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  29. mcp_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +232 -0
  30. mcp_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  31. mcp_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  32. mcp_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  33. mcp_agent/resources/examples/mcp/elicitations/forms_demo.py +111 -0
  34. mcp_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  35. mcp_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  36. mcp_agent/resources/examples/{prompting/agent.py → mcp/elicitations/tool_call.py} +4 -5
  37. mcp_agent/resources/examples/mcp/state-transfer/agent_two.py +1 -1
  38. mcp_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +1 -1
  39. mcp_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +1 -0
  40. mcp_agent/resources/examples/workflows/evaluator.py +1 -1
  41. mcp_agent/resources/examples/workflows/graded_report.md +89 -0
  42. mcp_agent/resources/examples/workflows/orchestrator.py +7 -9
  43. mcp_agent/resources/examples/workflows/parallel.py +0 -2
  44. mcp_agent/resources/examples/workflows/short_story.md +13 -0
  45. mcp_agent/resources/examples/in_dev/agent_build.py +0 -84
  46. mcp_agent/resources/examples/in_dev/css-LICENSE.txt +0 -21
  47. mcp_agent/resources/examples/in_dev/slides.py +0 -110
  48. mcp_agent/resources/examples/internal/agent.py +0 -20
  49. mcp_agent/resources/examples/internal/fastagent.config.yaml +0 -66
  50. mcp_agent/resources/examples/internal/history_transfer.py +0 -35
  51. mcp_agent/resources/examples/internal/job.py +0 -84
  52. mcp_agent/resources/examples/internal/prompt_category.py +0 -21
  53. mcp_agent/resources/examples/internal/prompt_sizing.py +0 -51
  54. mcp_agent/resources/examples/internal/simple.txt +0 -2
  55. mcp_agent/resources/examples/internal/sizer.py +0 -20
  56. mcp_agent/resources/examples/internal/social.py +0 -67
  57. mcp_agent/resources/examples/prompting/__init__.py +0 -3
  58. mcp_agent/resources/examples/prompting/delimited_prompt.txt +0 -14
  59. mcp_agent/resources/examples/prompting/fastagent.config.yaml +0 -43
  60. mcp_agent/resources/examples/prompting/image_server.py +0 -52
  61. mcp_agent/resources/examples/prompting/prompt1.txt +0 -6
  62. mcp_agent/resources/examples/prompting/work_with_image.py +0 -19
  63. {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.37.dist-info}/WHEEL +0 -0
  64. {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.37.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,59 @@
1
+ """Shared styling configuration for MCP elicitation forms."""
2
+
3
+ from prompt_toolkit.styles import Style
4
+
5
+ # Define consistent elicitation style - inspired by usage display and interactive prompt
6
+ ELICITATION_STYLE = Style.from_dict(
7
+ {
8
+ # Dialog structure - use ansidefault for true black, remove problematic shadow
9
+ "dialog": "bg:ansidefault", # True black dialog using ansidefault
10
+ "dialog.body": "bg:ansidefault fg:ansiwhite", # True black dialog body
11
+ "dialog shadow": "bg:ansidefault", # Set shadow background to match application
12
+ "dialog.border": "bg:ansidefault", # True black border background
13
+ # Set application background to true black
14
+ "application": "bg:ansidefault", # True black application background
15
+ # Title styling with better contrast
16
+ "title": "fg:ansibrightmagenta bold", # Bright magenta text
17
+ # Buttons - only define focused state to preserve focus highlighting
18
+ "button.focused": "bg:ansibrightgreen fg:ansiblack bold", # Bright green with black text for contrast
19
+ "button.arrow": "fg:ansiwhite bold", # White arrows for visibility
20
+ # Form elements with consistent green/yellow theme
21
+ # Checkboxes - green when checked, yellow when focused
22
+ "checkbox": "fg:ansidefault", # Default color unchecked checkbox (dimmer)
23
+ "checkbox-checked": "fg:ansibrightgreen bold", # Green when checked (matches buttons)
24
+ "checkbox-selected": "bg:ansidefault fg:ansibrightyellow bold", # Yellow when focused
25
+ # Radio list styling - consistent with checkbox colors
26
+ "radio-list": "bg:ansidefault", # True black background for radio list
27
+ "radio": "fg:ansidefault", # Default color for unselected items (dimmer)
28
+ "radio-selected": "bg:ansidefault fg:ansibrightyellow bold", # Yellow when focused
29
+ "radio-checked": "fg:ansibrightgreen bold", # Green when selected (matches buttons)
30
+ # Text input areas - use ansidefault for non-focused (dimmer effect)
31
+ "input-field": "fg:ansidefault bold", # Default color (inactive) - should be dimmer than white
32
+ "input-field.focused": "fg:ansibrightyellow bold", # Bright yellow (active)
33
+ "input-field.error": "fg:ansired bold", # Red text (validation error)
34
+ # Frame styling with ANSI colors - make borders visible
35
+ "frame.border": "fg:ansibrightblack", # Bright black borders for subtlety
36
+ "frame.label": "fg:ansigray", # Gray frame labels (less prominent)
37
+ # Labels and text - use white for good visibility
38
+ "label": "fg:ansiwhite", # White labels for good readability
39
+ "message": "fg:ansibrightcyan", # Bright cyan messages (no bold)
40
+ # Agent and server names - make them match
41
+ "agent-name": "fg:ansibrightblue bold",
42
+ "server-name": "fg:ansibrightblue bold", # Same color as agent
43
+ # Validation errors - better contrast
44
+ "validation-toolbar": "bg:ansibrightred fg:ansiwhite bold",
45
+ "validation-toolbar.text": "bg:ansibrightred fg:ansiwhite",
46
+ "validation.border": "fg:ansibrightred",
47
+ "validation-error": "fg:ansibrightred bold", # For status line errors
48
+ # Separator styling
49
+ "separator": "fg:ansibrightblue bold",
50
+ # Completion menu - exactly matching enhanced_prompt.py
51
+ "completion-menu.completion": "bg:ansiblack fg:ansigreen",
52
+ "completion-menu.completion.current": "bg:ansiblack fg:ansigreen bold",
53
+ "completion-menu.meta.completion": "bg:ansiblack fg:ansiblue",
54
+ "completion-menu.meta.completion.current": "bg:ansibrightblack fg:ansiblue",
55
+ # Toolbar - matching enhanced_prompt.py exactly
56
+ "bottom-toolbar": "fg:ansiblack bg:ansigray",
57
+ "bottom-toolbar.text": "fg:ansiblack bg:ansigray",
58
+ }
59
+ )
@@ -0,0 +1,88 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+ from mcp_agent.human_input.elicitation_form import (
4
+ show_simple_elicitation_form,
5
+ )
6
+ from mcp_agent.human_input.elicitation_forms import (
7
+ ELICITATION_STYLE,
8
+ )
9
+ from mcp_agent.human_input.elicitation_state import elicitation_state
10
+ from mcp_agent.human_input.types import (
11
+ HumanInputRequest,
12
+ HumanInputResponse,
13
+ )
14
+ from mcp_agent.progress_display import progress_display
15
+
16
+
17
+ async def elicitation_input_callback(
18
+ request: HumanInputRequest,
19
+ agent_name: str | None = None,
20
+ server_name: str | None = None,
21
+ server_info: dict[str, Any] | None = None,
22
+ ) -> HumanInputResponse:
23
+ """Request input from a human user for MCP server elicitation requests."""
24
+
25
+ # Extract effective names
26
+ effective_agent_name = agent_name or (
27
+ request.metadata.get("agent_name", "Unknown Agent") if request.metadata else "Unknown Agent"
28
+ )
29
+ effective_server_name = server_name or "Unknown Server"
30
+
31
+ # Check if elicitation is disabled for this server
32
+ if elicitation_state.is_disabled(effective_server_name):
33
+ return HumanInputResponse(
34
+ request_id=request.request_id,
35
+ response="__CANCELLED__",
36
+ metadata={"auto_cancelled": True, "reason": "Server elicitation disabled by user"},
37
+ )
38
+
39
+ # Get the elicitation schema from metadata
40
+ schema: Optional[Dict[str, Any]] = None
41
+ if request.metadata and "requested_schema" in request.metadata:
42
+ schema = request.metadata["requested_schema"]
43
+
44
+ # Use the context manager to pause the progress display while getting input
45
+ with progress_display.paused():
46
+ try:
47
+ if schema:
48
+ form_action, form_data = await show_simple_elicitation_form(
49
+ schema=schema,
50
+ message=request.prompt,
51
+ agent_name=effective_agent_name,
52
+ server_name=effective_server_name,
53
+ )
54
+
55
+ if form_action == "accept" and form_data is not None:
56
+ # Convert form data to JSON string
57
+ import json
58
+
59
+ response = json.dumps(form_data)
60
+ elif form_action == "decline":
61
+ response = "__DECLINED__"
62
+ elif form_action == "disable":
63
+ response = "__DISABLE_SERVER__"
64
+ else: # cancel
65
+ response = "__CANCELLED__"
66
+ else:
67
+ # No schema, fall back to text input using prompt_toolkit only
68
+ from prompt_toolkit.shortcuts import input_dialog
69
+
70
+ response = await input_dialog(
71
+ title="Input Requested",
72
+ text=f"Agent: {effective_agent_name}\nServer: {effective_server_name}\n\n{request.prompt}",
73
+ style=ELICITATION_STYLE,
74
+ ).run_async()
75
+
76
+ if response is None:
77
+ response = "__CANCELLED__"
78
+
79
+ except KeyboardInterrupt:
80
+ response = "__CANCELLED__"
81
+ except EOFError:
82
+ response = "__CANCELLED__"
83
+
84
+ return HumanInputResponse(
85
+ request_id=request.request_id,
86
+ response=response.strip() if isinstance(response, str) else response,
87
+ metadata={"has_schema": schema is not None},
88
+ )
@@ -0,0 +1,34 @@
1
+ """Simple state management for elicitation Cancel All functionality."""
2
+
3
+ from typing import Set
4
+
5
+
6
+ class ElicitationState:
7
+ """Manages global state for elicitation requests, including disabled servers."""
8
+
9
+ def __init__(self):
10
+ self.disabled_servers: Set[str] = set()
11
+
12
+ def disable_server(self, server_name: str) -> None:
13
+ """Disable elicitation requests for a specific server."""
14
+ self.disabled_servers.add(server_name)
15
+
16
+ def is_disabled(self, server_name: str) -> bool:
17
+ """Check if elicitation is disabled for a server."""
18
+ return server_name in self.disabled_servers
19
+
20
+ def enable_server(self, server_name: str) -> None:
21
+ """Re-enable elicitation requests for a specific server."""
22
+ self.disabled_servers.discard(server_name)
23
+
24
+ def clear_all(self) -> None:
25
+ """Clear all disabled servers."""
26
+ self.disabled_servers.clear()
27
+
28
+ def get_disabled_servers(self) -> Set[str]:
29
+ """Get a copy of all disabled servers."""
30
+ return self.disabled_servers.copy()
31
+
32
+
33
+ # Global instance for session-scoped Cancel All functionality
34
+ elicitation_state = ElicitationState()
@@ -242,8 +242,10 @@ class GoogleNativeAugmentedLLM(AugmentedLLM[types.Content, types.Content]):
242
242
  self.history.get(include_completion_history=True)
243
243
  )
244
244
  else:
245
- # If not using history, start with an empty list
246
- conversation_history = []
245
+ # If not using history, convert the last message to google.genai format
246
+ conversation_history = self._converter.convert_to_google_content(
247
+ self.history.get(include_completion_history=True)[-1:]
248
+ )
247
249
 
248
250
  self.logger.debug(f"Google completion requested with messages: {conversation_history}")
249
251
  self._log_chat_progress(
@@ -123,7 +123,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
123
123
  # For non-OpenAI providers (like Ollama), ChatCompletionStreamState might not work correctly
124
124
  # Fall back to manual accumulation if needed
125
125
  # TODO -- consider this and whether to subclass instead
126
- if self.provider in [Provider.GENERIC, Provider.OPENROUTER]:
126
+ if self.provider in [Provider.GENERIC, Provider.OPENROUTER, Provider.GOOGLE_OAI]:
127
127
  return await self._process_stream_manual(stream, model)
128
128
 
129
129
  # Use ChatCompletionStreamState helper for accumulation (OpenAI only)
@@ -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
@@ -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: