fast-agent-mcp 0.2.36__py3-none-any.whl → 0.2.38__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.
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/METADATA +10 -7
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/RECORD +45 -47
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/licenses/LICENSE +1 -1
- mcp_agent/cli/commands/quickstart.py +60 -5
- mcp_agent/config.py +10 -0
- mcp_agent/context.py +1 -4
- mcp_agent/core/agent_types.py +7 -6
- mcp_agent/core/direct_decorators.py +14 -0
- mcp_agent/core/direct_factory.py +1 -0
- mcp_agent/core/fastagent.py +23 -2
- mcp_agent/human_input/elicitation_form.py +723 -0
- mcp_agent/human_input/elicitation_forms.py +59 -0
- mcp_agent/human_input/elicitation_handler.py +88 -0
- mcp_agent/human_input/elicitation_state.py +34 -0
- mcp_agent/llm/providers/augmented_llm_google_native.py +4 -2
- mcp_agent/llm/providers/augmented_llm_openai.py +1 -1
- mcp_agent/mcp/elicitation_factory.py +84 -0
- mcp_agent/mcp/elicitation_handlers.py +155 -0
- mcp_agent/mcp/helpers/content_helpers.py +27 -0
- mcp_agent/mcp/helpers/server_config_helpers.py +10 -8
- mcp_agent/mcp/mcp_agent_client_session.py +44 -1
- mcp_agent/mcp/mcp_aggregator.py +56 -11
- mcp_agent/mcp/mcp_connection_manager.py +30 -18
- mcp_agent/mcp_server/agent_server.py +2 -0
- mcp_agent/mcp_server_registry.py +16 -8
- mcp_agent/resources/examples/data-analysis/analysis.py +1 -2
- mcp_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- mcp_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +232 -0
- mcp_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- mcp_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- mcp_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- mcp_agent/resources/examples/mcp/elicitations/forms_demo.py +111 -0
- mcp_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- mcp_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- mcp_agent/resources/examples/{prompting/agent.py → mcp/elicitations/tool_call.py} +4 -5
- mcp_agent/resources/examples/mcp/state-transfer/agent_two.py +1 -1
- mcp_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +1 -1
- mcp_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +1 -0
- mcp_agent/resources/examples/workflows/evaluator.py +1 -1
- mcp_agent/resources/examples/workflows/graded_report.md +89 -0
- mcp_agent/resources/examples/workflows/orchestrator.py +7 -9
- mcp_agent/resources/examples/workflows/parallel.py +0 -2
- mcp_agent/resources/examples/workflows/short_story.md +13 -0
- mcp_agent/resources/examples/in_dev/agent_build.py +0 -84
- mcp_agent/resources/examples/in_dev/css-LICENSE.txt +0 -21
- mcp_agent/resources/examples/in_dev/slides.py +0 -110
- mcp_agent/resources/examples/internal/agent.py +0 -20
- mcp_agent/resources/examples/internal/fastagent.config.yaml +0 -66
- mcp_agent/resources/examples/internal/history_transfer.py +0 -35
- mcp_agent/resources/examples/internal/job.py +0 -84
- mcp_agent/resources/examples/internal/prompt_category.py +0 -21
- mcp_agent/resources/examples/internal/prompt_sizing.py +0 -51
- mcp_agent/resources/examples/internal/simple.txt +0 -2
- mcp_agent/resources/examples/internal/sizer.py +0 -20
- mcp_agent/resources/examples/internal/social.py +0 -67
- mcp_agent/resources/examples/prompting/__init__.py +0 -3
- mcp_agent/resources/examples/prompting/delimited_prompt.txt +0 -14
- mcp_agent/resources/examples/prompting/fastagent.config.yaml +0 -43
- mcp_agent/resources/examples/prompting/image_server.py +0 -52
- mcp_agent/resources/examples/prompting/prompt1.txt +0 -6
- mcp_agent/resources/examples/prompting/work_with_image.py +0 -19
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.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,
|
|
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:
|
|
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
|
|
20
|
-
|
|
21
|
-
ctx.session.server_config):
|
|
22
|
-
|
|
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
|
|
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:
|