fast-agent-mcp 0.4.7__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.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for MCP stdio client integration with our logging system.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import os
|
|
7
|
+
from typing import TextIO
|
|
8
|
+
|
|
9
|
+
from fast_agent.core.logging.logger import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LoggerTextIO(TextIO):
|
|
15
|
+
"""
|
|
16
|
+
A TextIO implementation that logs to our application logger.
|
|
17
|
+
This implements the full TextIO interface as specified by Python.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
server_name: The name of the server to include in logs
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, server_name: str) -> None:
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.server_name = server_name
|
|
26
|
+
# Use a StringIO for buffering
|
|
27
|
+
self._buffer = io.StringIO()
|
|
28
|
+
# Keep track of complete and partial lines
|
|
29
|
+
self._line_buffer = ""
|
|
30
|
+
|
|
31
|
+
def write(self, s: str) -> int:
|
|
32
|
+
"""
|
|
33
|
+
Write data to our buffer and log any complete lines.
|
|
34
|
+
"""
|
|
35
|
+
if not s:
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
# Handle line buffering for clean log output
|
|
39
|
+
text = self._line_buffer + s
|
|
40
|
+
lines = text.split("\n")
|
|
41
|
+
|
|
42
|
+
# If the text ends with a newline, the last line is complete
|
|
43
|
+
if text.endswith("\n"):
|
|
44
|
+
complete_lines = lines
|
|
45
|
+
self._line_buffer = ""
|
|
46
|
+
else:
|
|
47
|
+
# Otherwise, the last line is incomplete
|
|
48
|
+
complete_lines = lines[:-1]
|
|
49
|
+
self._line_buffer = lines[-1]
|
|
50
|
+
|
|
51
|
+
# Log complete lines but at debug level instead of info to prevent console spam
|
|
52
|
+
for line in complete_lines:
|
|
53
|
+
if line.strip(): # Only log non-empty lines
|
|
54
|
+
logger.debug(f"{self.server_name} (stderr): {line}")
|
|
55
|
+
|
|
56
|
+
# Always write to the underlying buffer
|
|
57
|
+
return self._buffer.write(s)
|
|
58
|
+
|
|
59
|
+
def flush(self) -> None:
|
|
60
|
+
"""Flush the internal buffer."""
|
|
61
|
+
self._buffer.flush()
|
|
62
|
+
|
|
63
|
+
def close(self) -> None:
|
|
64
|
+
"""Close the stream."""
|
|
65
|
+
# Log any remaining content in the line buffer
|
|
66
|
+
if self._line_buffer and self._line_buffer.strip():
|
|
67
|
+
logger.debug(f"{self.server_name} (stderr): {self._line_buffer}")
|
|
68
|
+
self._buffer.close()
|
|
69
|
+
|
|
70
|
+
def readable(self) -> bool:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def writable(self) -> bool:
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
def seekable(self) -> bool:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def fileno(self) -> int:
|
|
80
|
+
"""
|
|
81
|
+
Return a file descriptor for /dev/null.
|
|
82
|
+
This prevents output from showing on the terminal
|
|
83
|
+
while still allowing our write() method to capture it for logging.
|
|
84
|
+
"""
|
|
85
|
+
if not hasattr(self, "_devnull_fd"):
|
|
86
|
+
self._devnull_fd = os.open(os.devnull, os.O_WRONLY)
|
|
87
|
+
return self._devnull_fd
|
|
88
|
+
|
|
89
|
+
def __del__(self):
|
|
90
|
+
"""Clean up the devnull file descriptor."""
|
|
91
|
+
if hasattr(self, "_devnull_fd"):
|
|
92
|
+
try:
|
|
93
|
+
os.close(self._devnull_fd)
|
|
94
|
+
except (OSError, AttributeError):
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_stderr_handler(server_name: str) -> TextIO:
|
|
99
|
+
"""
|
|
100
|
+
Get a stderr handler that routes MCP server errors to our logger.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
server_name: The name of the server to include in logs
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A TextIO object that can be used as stderr by MCP
|
|
107
|
+
"""
|
|
108
|
+
return LoggerTextIO(server_name)
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A derived client session for the MCP Agent framework.
|
|
3
|
+
It adds logging and supports sampling requests.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from mcp import ClientSession, ServerNotification
|
|
10
|
+
from mcp.shared.message import MessageMetadata
|
|
11
|
+
from mcp.shared.session import (
|
|
12
|
+
ProgressFnT,
|
|
13
|
+
ReceiveResultT,
|
|
14
|
+
SendRequestT,
|
|
15
|
+
)
|
|
16
|
+
from mcp.types import (
|
|
17
|
+
CallToolRequest,
|
|
18
|
+
CallToolRequestParams,
|
|
19
|
+
CallToolResult,
|
|
20
|
+
GetPromptRequest,
|
|
21
|
+
GetPromptRequestParams,
|
|
22
|
+
GetPromptResult,
|
|
23
|
+
Implementation,
|
|
24
|
+
ListRootsResult,
|
|
25
|
+
ReadResourceRequest,
|
|
26
|
+
ReadResourceRequestParams,
|
|
27
|
+
ReadResourceResult,
|
|
28
|
+
Root,
|
|
29
|
+
ToolListChangedNotification,
|
|
30
|
+
)
|
|
31
|
+
from pydantic import FileUrl
|
|
32
|
+
|
|
33
|
+
from fast_agent.context_dependent import ContextDependent
|
|
34
|
+
from fast_agent.core.logging.logger import get_logger
|
|
35
|
+
from fast_agent.mcp.helpers.server_config_helpers import get_server_config
|
|
36
|
+
from fast_agent.mcp.sampling import sample
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from fast_agent.config import MCPServerSettings
|
|
40
|
+
from fast_agent.mcp.transport_tracking import TransportChannelMetrics
|
|
41
|
+
|
|
42
|
+
logger = get_logger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def list_roots(ctx: ClientSession) -> ListRootsResult:
|
|
46
|
+
"""List roots callback that will be called by the MCP library."""
|
|
47
|
+
|
|
48
|
+
if server_config := get_server_config(ctx):
|
|
49
|
+
if server_config.roots:
|
|
50
|
+
roots = [
|
|
51
|
+
Root(
|
|
52
|
+
uri=FileUrl(
|
|
53
|
+
root.server_uri_alias or root.uri,
|
|
54
|
+
),
|
|
55
|
+
name=root.name,
|
|
56
|
+
)
|
|
57
|
+
for root in server_config.roots
|
|
58
|
+
]
|
|
59
|
+
return ListRootsResult(roots=roots)
|
|
60
|
+
|
|
61
|
+
return ListRootsResult(roots=[])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MCPAgentClientSession(ClientSession, ContextDependent):
|
|
65
|
+
"""
|
|
66
|
+
MCP Agent framework acts as a client to the servers providing tools/resources/prompts for the agent workloads.
|
|
67
|
+
This is a simple client session for those server connections, and supports
|
|
68
|
+
- handling sampling requests
|
|
69
|
+
- notifications
|
|
70
|
+
- MCP root configuration
|
|
71
|
+
|
|
72
|
+
Developers can extend this class to add more custom functionality as needed
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
76
|
+
# Extract server_name if provided in kwargs
|
|
77
|
+
from importlib.metadata import version
|
|
78
|
+
|
|
79
|
+
self.session_server_name = kwargs.pop("server_name", None)
|
|
80
|
+
# Extract the notification callbacks if provided
|
|
81
|
+
self._tool_list_changed_callback = kwargs.pop("tool_list_changed_callback", None)
|
|
82
|
+
# Extract server_config if provided
|
|
83
|
+
self.server_config: MCPServerSettings | None = kwargs.pop("server_config", None)
|
|
84
|
+
# Extract agent_model if provided (for auto_sampling fallback)
|
|
85
|
+
self.agent_model: str | None = kwargs.pop("agent_model", None)
|
|
86
|
+
# Extract agent_name if provided
|
|
87
|
+
self.agent_name: str | None = kwargs.pop("agent_name", None)
|
|
88
|
+
# Extract api_key if provided
|
|
89
|
+
self.api_key: str | None = kwargs.pop("api_key", None)
|
|
90
|
+
# Extract custom elicitation handler if provided
|
|
91
|
+
custom_elicitation_handler = kwargs.pop("elicitation_handler", None)
|
|
92
|
+
# Extract optional context for ContextDependent mixin without passing it to ClientSession
|
|
93
|
+
self._context = kwargs.pop("context", None)
|
|
94
|
+
# Extract transport metrics tracker if provided
|
|
95
|
+
self._transport_metrics: TransportChannelMetrics | None = kwargs.pop(
|
|
96
|
+
"transport_metrics", None
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Track the effective elicitation mode for diagnostics
|
|
100
|
+
self.effective_elicitation_mode: str | None = "none"
|
|
101
|
+
|
|
102
|
+
version = version("fast-agent-mcp") or "dev"
|
|
103
|
+
fast_agent: Implementation = Implementation(name="fast-agent-mcp", version=version)
|
|
104
|
+
if self.server_config and self.server_config.implementation:
|
|
105
|
+
fast_agent = self.server_config.implementation
|
|
106
|
+
|
|
107
|
+
# Only register callbacks if the server_config has the relevant settings
|
|
108
|
+
list_roots_cb = list_roots if (self.server_config and self.server_config.roots) else None
|
|
109
|
+
|
|
110
|
+
# Register sampling callback if either:
|
|
111
|
+
# 1. Sampling is explicitly configured, OR
|
|
112
|
+
# 2. Application-level auto_sampling is enabled
|
|
113
|
+
sampling_cb = None
|
|
114
|
+
if self.server_config and self.server_config.sampling:
|
|
115
|
+
# Explicit sampling configuration
|
|
116
|
+
sampling_cb = sample
|
|
117
|
+
elif self._should_enable_auto_sampling():
|
|
118
|
+
# Auto-sampling enabled at application level
|
|
119
|
+
sampling_cb = sample
|
|
120
|
+
|
|
121
|
+
# Use custom elicitation handler if provided, otherwise resolve using factory
|
|
122
|
+
if custom_elicitation_handler is not None:
|
|
123
|
+
elicitation_handler = custom_elicitation_handler
|
|
124
|
+
else:
|
|
125
|
+
# Try to resolve using factory
|
|
126
|
+
elicitation_handler = None
|
|
127
|
+
try:
|
|
128
|
+
from fast_agent.agents.agent_types import AgentConfig
|
|
129
|
+
from fast_agent.context import get_current_context
|
|
130
|
+
from fast_agent.mcp.elicitation_factory import resolve_elicitation_handler
|
|
131
|
+
|
|
132
|
+
context = get_current_context()
|
|
133
|
+
if context and context.config:
|
|
134
|
+
# Create a minimal agent config for the factory
|
|
135
|
+
agent_config = AgentConfig(
|
|
136
|
+
name=self.agent_name or "unknown",
|
|
137
|
+
model=self.agent_model or "unknown",
|
|
138
|
+
elicitation_handler=None,
|
|
139
|
+
)
|
|
140
|
+
elicitation_handler = resolve_elicitation_handler(
|
|
141
|
+
agent_config, context.config, self.server_config
|
|
142
|
+
)
|
|
143
|
+
except Exception:
|
|
144
|
+
# If factory resolution fails, we'll use default fallback
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
# Fallback to forms handler only if factory resolution wasn't attempted
|
|
148
|
+
if elicitation_handler is None and not self.server_config:
|
|
149
|
+
from fast_agent.mcp.elicitation_handlers import forms_elicitation_handler
|
|
150
|
+
|
|
151
|
+
elicitation_handler = forms_elicitation_handler
|
|
152
|
+
|
|
153
|
+
# Determine effective elicitation mode for diagnostics
|
|
154
|
+
if self.server_config and getattr(self.server_config, "elicitation", None):
|
|
155
|
+
self.effective_elicitation_mode = self.server_config.elicitation.mode or "forms"
|
|
156
|
+
elif elicitation_handler is not None:
|
|
157
|
+
# Use global config if available to distinguish auto-cancel
|
|
158
|
+
try:
|
|
159
|
+
from fast_agent.context import get_current_context
|
|
160
|
+
|
|
161
|
+
context = get_current_context()
|
|
162
|
+
mode = None
|
|
163
|
+
if context and getattr(context, "config", None):
|
|
164
|
+
elicitation_cfg = getattr(context.config, "elicitation", None)
|
|
165
|
+
if isinstance(elicitation_cfg, dict):
|
|
166
|
+
mode = elicitation_cfg.get("mode")
|
|
167
|
+
else:
|
|
168
|
+
mode = getattr(elicitation_cfg, "mode", None)
|
|
169
|
+
self.effective_elicitation_mode = (mode or "forms").lower()
|
|
170
|
+
except Exception:
|
|
171
|
+
self.effective_elicitation_mode = "forms"
|
|
172
|
+
else:
|
|
173
|
+
self.effective_elicitation_mode = "none"
|
|
174
|
+
|
|
175
|
+
super().__init__(
|
|
176
|
+
*args,
|
|
177
|
+
**kwargs,
|
|
178
|
+
list_roots_callback=list_roots_cb,
|
|
179
|
+
sampling_callback=sampling_cb,
|
|
180
|
+
client_info=fast_agent,
|
|
181
|
+
elicitation_callback=elicitation_handler,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def _should_enable_auto_sampling(self) -> bool:
|
|
185
|
+
"""Check if auto_sampling is enabled at the application level."""
|
|
186
|
+
try:
|
|
187
|
+
from fast_agent.context import get_current_context
|
|
188
|
+
|
|
189
|
+
context = get_current_context()
|
|
190
|
+
if context and context.config:
|
|
191
|
+
return getattr(context.config, "auto_sampling", True)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
return True # Default to True if can't access config
|
|
195
|
+
|
|
196
|
+
async def send_request(
|
|
197
|
+
self,
|
|
198
|
+
request: SendRequestT,
|
|
199
|
+
result_type: type[ReceiveResultT],
|
|
200
|
+
request_read_timeout_seconds: timedelta | None = None,
|
|
201
|
+
metadata: MessageMetadata | None = None,
|
|
202
|
+
progress_callback: ProgressFnT | None = None,
|
|
203
|
+
) -> ReceiveResultT:
|
|
204
|
+
logger.debug("send_request: request=", data=request.model_dump())
|
|
205
|
+
request_id = getattr(self, "_request_id", None)
|
|
206
|
+
try:
|
|
207
|
+
result = await super().send_request(
|
|
208
|
+
request=request,
|
|
209
|
+
result_type=result_type,
|
|
210
|
+
request_read_timeout_seconds=request_read_timeout_seconds,
|
|
211
|
+
metadata=metadata,
|
|
212
|
+
progress_callback=progress_callback,
|
|
213
|
+
)
|
|
214
|
+
logger.debug(
|
|
215
|
+
"send_request: response=",
|
|
216
|
+
data=result.model_dump() if result is not None else "no response returned",
|
|
217
|
+
)
|
|
218
|
+
self._attach_transport_channel(request_id, result)
|
|
219
|
+
return result
|
|
220
|
+
except Exception as e:
|
|
221
|
+
from anyio import ClosedResourceError
|
|
222
|
+
|
|
223
|
+
from fast_agent.core.exceptions import ServerSessionTerminatedError
|
|
224
|
+
|
|
225
|
+
# Check for session terminated error (404 from server)
|
|
226
|
+
if self._is_session_terminated_error(e):
|
|
227
|
+
raise ServerSessionTerminatedError(
|
|
228
|
+
server_name=self.session_server_name or "unknown",
|
|
229
|
+
details="Server returned 404 - session may have expired due to server restart",
|
|
230
|
+
) from e
|
|
231
|
+
|
|
232
|
+
# Handle connection closure errors (transport closed)
|
|
233
|
+
if isinstance(e, ClosedResourceError):
|
|
234
|
+
from fast_agent.ui import console
|
|
235
|
+
|
|
236
|
+
console.console.print(
|
|
237
|
+
f"[dim red]MCP server {self.session_server_name} offline[/dim red]"
|
|
238
|
+
)
|
|
239
|
+
raise ConnectionError(f"MCP server {self.session_server_name} offline") from e
|
|
240
|
+
|
|
241
|
+
logger.error(f"send_request failed: {str(e)}")
|
|
242
|
+
raise
|
|
243
|
+
|
|
244
|
+
def _is_session_terminated_error(self, exc: Exception) -> bool:
|
|
245
|
+
"""Check if exception is a session terminated error (code 32600 from 404)."""
|
|
246
|
+
from mcp.shared.exceptions import McpError
|
|
247
|
+
|
|
248
|
+
from fast_agent.core.exceptions import ServerSessionTerminatedError
|
|
249
|
+
|
|
250
|
+
if isinstance(exc, McpError):
|
|
251
|
+
error_data = getattr(exc, "error", None)
|
|
252
|
+
if error_data:
|
|
253
|
+
code = getattr(error_data, "code", None)
|
|
254
|
+
if code == ServerSessionTerminatedError.SESSION_TERMINATED_CODE:
|
|
255
|
+
return True
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def _attach_transport_channel(self, request_id, result) -> None:
|
|
259
|
+
if self._transport_metrics is None or request_id is None or result is None:
|
|
260
|
+
return
|
|
261
|
+
channel = self._transport_metrics.consume_response_channel(request_id)
|
|
262
|
+
if not channel:
|
|
263
|
+
return
|
|
264
|
+
try:
|
|
265
|
+
setattr(result, "transport_channel", channel)
|
|
266
|
+
except Exception:
|
|
267
|
+
# If result cannot be mutated, ignore silently
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
async def _received_notification(self, notification: ServerNotification) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Can be overridden by subclasses to handle a notification without needing
|
|
273
|
+
to listen on the message stream.
|
|
274
|
+
"""
|
|
275
|
+
logger.debug(
|
|
276
|
+
"_received_notification: notification=",
|
|
277
|
+
data=notification.model_dump(),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Call parent notification handler first
|
|
281
|
+
await super()._received_notification(notification)
|
|
282
|
+
|
|
283
|
+
# Then process our specific notification types
|
|
284
|
+
match notification.root:
|
|
285
|
+
case ToolListChangedNotification():
|
|
286
|
+
# Simple notification handling - just call the callback if it exists
|
|
287
|
+
if self._tool_list_changed_callback and self.session_server_name:
|
|
288
|
+
logger.info(
|
|
289
|
+
f"Tool list changed for server '{self.session_server_name}', triggering callback"
|
|
290
|
+
)
|
|
291
|
+
# Use asyncio.create_task to prevent blocking the notification handler
|
|
292
|
+
import asyncio
|
|
293
|
+
|
|
294
|
+
asyncio.create_task(
|
|
295
|
+
self._handle_tool_list_change_callback(self.session_server_name)
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
logger.debug(
|
|
299
|
+
f"Tool list changed for server '{self.session_server_name}' but no callback registered"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
async def _handle_tool_list_change_callback(self, server_name: str) -> None:
|
|
305
|
+
"""
|
|
306
|
+
Helper method to handle tool list change callback in a separate task
|
|
307
|
+
to prevent blocking the notification handler
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
await self._tool_list_changed_callback(server_name)
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"Error in tool list changed callback: {e}")
|
|
313
|
+
|
|
314
|
+
# TODO -- decide whether to make this override type safe or not (modify SDK)
|
|
315
|
+
async def call_tool(
|
|
316
|
+
self,
|
|
317
|
+
name: str,
|
|
318
|
+
arguments: dict | None = None,
|
|
319
|
+
_meta: dict | None = None,
|
|
320
|
+
progress_callback: ProgressFnT | None = None,
|
|
321
|
+
**kwargs,
|
|
322
|
+
) -> CallToolResult:
|
|
323
|
+
"""Call a tool with optional metadata and progress callback support.
|
|
324
|
+
|
|
325
|
+
Always uses our overridden send_request to ensure session terminated errors
|
|
326
|
+
are properly detected and converted to ServerSessionTerminatedError.
|
|
327
|
+
"""
|
|
328
|
+
from mcp.types import RequestParams
|
|
329
|
+
|
|
330
|
+
# Always create request ourselves to ensure we go through our send_request override
|
|
331
|
+
# This is critical for session terminated detection to work
|
|
332
|
+
params = CallToolRequestParams(name=name, arguments=arguments)
|
|
333
|
+
|
|
334
|
+
if _meta:
|
|
335
|
+
# Safe merge - preserve existing meta fields like progressToken
|
|
336
|
+
existing_meta = kwargs.get("meta")
|
|
337
|
+
if existing_meta:
|
|
338
|
+
meta_dict = (
|
|
339
|
+
existing_meta.model_dump() if hasattr(existing_meta, "model_dump") else {}
|
|
340
|
+
)
|
|
341
|
+
meta_dict.update(_meta)
|
|
342
|
+
meta_obj = RequestParams.Meta(**meta_dict)
|
|
343
|
+
else:
|
|
344
|
+
meta_obj = RequestParams.Meta(**_meta)
|
|
345
|
+
|
|
346
|
+
params_dict = params.model_dump(by_alias=True)
|
|
347
|
+
params_dict["_meta"] = meta_obj.model_dump()
|
|
348
|
+
params = CallToolRequestParams.model_validate(params_dict)
|
|
349
|
+
|
|
350
|
+
request = CallToolRequest(method="tools/call", params=params)
|
|
351
|
+
return await self.send_request(
|
|
352
|
+
request, CallToolResult, progress_callback=progress_callback
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
async def read_resource(
|
|
356
|
+
self, uri: str, _meta: dict | None = None, **kwargs
|
|
357
|
+
) -> ReadResourceResult:
|
|
358
|
+
"""Read a resource with optional metadata support.
|
|
359
|
+
|
|
360
|
+
Always uses our overridden send_request to ensure session terminated errors
|
|
361
|
+
are properly detected and converted to ServerSessionTerminatedError.
|
|
362
|
+
"""
|
|
363
|
+
from mcp.types import RequestParams
|
|
364
|
+
|
|
365
|
+
# Always create request ourselves to ensure we go through our send_request override
|
|
366
|
+
params = ReadResourceRequestParams(uri=uri)
|
|
367
|
+
|
|
368
|
+
if _meta:
|
|
369
|
+
# Safe merge - preserve existing meta fields like progressToken
|
|
370
|
+
existing_meta = kwargs.get("meta")
|
|
371
|
+
if existing_meta:
|
|
372
|
+
meta_dict = (
|
|
373
|
+
existing_meta.model_dump() if hasattr(existing_meta, "model_dump") else {}
|
|
374
|
+
)
|
|
375
|
+
meta_dict.update(_meta)
|
|
376
|
+
meta_obj = RequestParams.Meta(**meta_dict)
|
|
377
|
+
else:
|
|
378
|
+
meta_obj = RequestParams.Meta(**_meta)
|
|
379
|
+
params = ReadResourceRequestParams(uri=uri, meta=meta_obj)
|
|
380
|
+
|
|
381
|
+
request = ReadResourceRequest(method="resources/read", params=params)
|
|
382
|
+
return await self.send_request(request, ReadResourceResult)
|
|
383
|
+
|
|
384
|
+
async def get_prompt(
|
|
385
|
+
self, name: str, arguments: dict | None = None, _meta: dict | None = None, **kwargs
|
|
386
|
+
) -> GetPromptResult:
|
|
387
|
+
"""Get a prompt with optional metadata support.
|
|
388
|
+
|
|
389
|
+
Always uses our overridden send_request to ensure session terminated errors
|
|
390
|
+
are properly detected and converted to ServerSessionTerminatedError.
|
|
391
|
+
"""
|
|
392
|
+
from mcp.types import RequestParams
|
|
393
|
+
|
|
394
|
+
# Always create request ourselves to ensure we go through our send_request override
|
|
395
|
+
params = GetPromptRequestParams(name=name, arguments=arguments)
|
|
396
|
+
|
|
397
|
+
if _meta:
|
|
398
|
+
# Safe merge - preserve existing meta fields like progressToken
|
|
399
|
+
existing_meta = kwargs.get("meta")
|
|
400
|
+
if existing_meta:
|
|
401
|
+
meta_dict = (
|
|
402
|
+
existing_meta.model_dump() if hasattr(existing_meta, "model_dump") else {}
|
|
403
|
+
)
|
|
404
|
+
meta_dict.update(_meta)
|
|
405
|
+
meta_obj = RequestParams.Meta(**meta_dict)
|
|
406
|
+
else:
|
|
407
|
+
meta_obj = RequestParams.Meta(**_meta)
|
|
408
|
+
params = GetPromptRequestParams(name=name, arguments=arguments, meta=meta_obj)
|
|
409
|
+
|
|
410
|
+
request = GetPromptRequest(method="prompts/get", params=params)
|
|
411
|
+
return await self.send_request(request, GetPromptResult)
|