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.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. 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)