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.
- {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/METADATA +15 -12
- {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/RECORD +55 -56
- {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/licenses/LICENSE +1 -1
- mcp_agent/agents/base_agent.py +2 -2
- mcp_agent/agents/workflow/router_agent.py +1 -1
- mcp_agent/cli/commands/quickstart.py +59 -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/enhanced_prompt.py +73 -13
- mcp_agent/core/fastagent.py +23 -2
- mcp_agent/core/interactive_prompt.py +118 -8
- 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/augmented_llm.py +31 -0
- mcp_agent/llm/providers/augmented_llm_anthropic.py +11 -23
- mcp_agent/llm/providers/augmented_llm_azure.py +4 -4
- mcp_agent/llm/providers/augmented_llm_google_native.py +4 -2
- mcp_agent/llm/providers/augmented_llm_openai.py +195 -12
- mcp_agent/llm/providers/multipart_converter_openai.py +4 -3
- 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/interfaces.py +1 -1
- 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/README.md +157 -0
- 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.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/WHEEL +0 -0
- {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:
|
|
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
|
mcp_agent/mcp/interfaces.py
CHANGED
|
@@ -21,7 +21,7 @@ from typing import (
|
|
|
21
21
|
runtime_checkable,
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
from
|
|
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
|
|
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:
|
mcp_agent/mcp/mcp_aggregator.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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.
|
|
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."""
|
mcp_agent/mcp_server_registry.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
+
server_config=config,
|
|
220
228
|
)
|
|
221
229
|
async with session:
|
|
222
230
|
logger.info(f"{server_name}: Connected to server using HTTP transport.")
|