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
fast_agent/config.py ADDED
@@ -0,0 +1,798 @@
1
+ """
2
+ Reading settings from environment variables and providing a settings object
3
+ for the application configuration.
4
+ """
5
+
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any, Literal
10
+
11
+ # Importing the MCP Implementation type eagerly pulls in the full MCP server
12
+ # stack (uvicorn, Starlette, etc.) which slows down startup. We only need the
13
+ # type for annotations, so avoid the runtime import.
14
+ if TYPE_CHECKING:
15
+ from mcp import Implementation
16
+ else: # pragma: no cover - used only to satisfy type checkers
17
+ Implementation = Any
18
+ from pydantic import BaseModel, ConfigDict, field_validator, model_validator
19
+ from pydantic_settings import BaseSettings, SettingsConfigDict
20
+
21
+
22
+ class MCPServerAuthSettings(BaseModel):
23
+ """Represents authentication configuration for a server.
24
+
25
+ Minimal OAuth v2.1 support with sensible defaults.
26
+ """
27
+
28
+ # Enable OAuth for SSE/HTTP transports. If None is provided for the auth block,
29
+ # the system will assume OAuth is enabled by default.
30
+ oauth: bool = True
31
+
32
+ # Local callback server configuration
33
+ redirect_port: int = 3030
34
+ redirect_path: str = "/callback"
35
+
36
+ # Optional scope override. If set to a list, values are space-joined.
37
+ scope: str | list[str] | None = None
38
+
39
+ # Token persistence: use OS keychain via 'keyring' by default; fallback to 'memory'.
40
+ persist: Literal["keyring", "memory"] = "keyring"
41
+
42
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
43
+
44
+
45
+ class MCPSamplingSettings(BaseModel):
46
+ model: str = "gpt-5-mini.low"
47
+
48
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
49
+
50
+
51
+ class MCPElicitationSettings(BaseModel):
52
+ mode: Literal["forms", "auto-cancel", "none"] = "none"
53
+ """Elicitation mode: 'forms' (default UI), 'auto-cancel', 'none' (no capability)"""
54
+
55
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
56
+
57
+
58
+ class MCPTimelineSettings(BaseModel):
59
+ """Configuration for MCP activity timeline display."""
60
+
61
+ steps: int = 20
62
+ """Number of timeline buckets to render."""
63
+
64
+ step_seconds: int = 30
65
+ """Duration of each timeline bucket in seconds."""
66
+
67
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
68
+
69
+ @staticmethod
70
+ def _parse_duration(value: str) -> int:
71
+ """Parse simple duration strings like '30s', '2m', '1h' into seconds."""
72
+ pattern = re.compile(r"^\s*(\d+)\s*([smhd]?)\s*$", re.IGNORECASE)
73
+ match = pattern.match(value)
74
+ if not match:
75
+ raise ValueError("Expected duration in seconds (e.g. 30, '45s', '2m').")
76
+ amount = int(match.group(1))
77
+ unit = match.group(2).lower()
78
+ multiplier = {
79
+ "": 1,
80
+ "s": 1,
81
+ "m": 60,
82
+ "h": 3600,
83
+ "d": 86400,
84
+ }.get(unit)
85
+ if multiplier is None:
86
+ raise ValueError("Duration unit must be one of s, m, h, or d.")
87
+ return amount * multiplier
88
+
89
+ @field_validator("steps", mode="before")
90
+ @classmethod
91
+ def _coerce_steps(cls, value: Any) -> int:
92
+ if isinstance(value, str):
93
+ if not value.strip().isdigit():
94
+ raise ValueError("Timeline steps must be a positive integer.")
95
+ value = int(value.strip())
96
+ elif isinstance(value, float):
97
+ value = int(value)
98
+ if not isinstance(value, int):
99
+ raise TypeError("Timeline steps must be an integer.")
100
+ if value <= 0:
101
+ raise ValueError("Timeline steps must be greater than zero.")
102
+ return value
103
+
104
+
105
+ class SkillsSettings(BaseModel):
106
+ """Configuration for the skills directory override."""
107
+
108
+ directory: str | None = None
109
+
110
+ model_config = ConfigDict(extra="ignore")
111
+
112
+
113
+ class ShellSettings(BaseModel):
114
+ """Configuration for shell execution behavior."""
115
+
116
+ timeout_seconds: int = 90
117
+ """Maximum seconds to wait for command output before terminating (default: 90s)"""
118
+
119
+ warning_interval_seconds: int = 30
120
+ """Show timeout warnings every N seconds (default: 30s)"""
121
+
122
+ model_config = ConfigDict(extra="ignore")
123
+
124
+ @field_validator("timeout_seconds", mode="before")
125
+ @classmethod
126
+ def _coerce_timeout(cls, value: Any) -> int:
127
+ """Support duration strings like '90s', '2m', '1h'"""
128
+ if isinstance(value, str):
129
+ return MCPTimelineSettings._parse_duration(value)
130
+ return int(value)
131
+
132
+ @field_validator("warning_interval_seconds", mode="before")
133
+ @classmethod
134
+ def _coerce_warning_interval(cls, value: Any) -> int:
135
+ """Support duration strings like '30s', '1m'"""
136
+ if isinstance(value, str):
137
+ return MCPTimelineSettings._parse_duration(value)
138
+ return int(value)
139
+
140
+
141
+ class MCPRootSettings(BaseModel):
142
+ """Represents a root directory configuration for an MCP server."""
143
+
144
+ uri: str
145
+ """The URI identifying the root. Must start with file://"""
146
+
147
+ name: str | None = None
148
+ """Optional name for the root."""
149
+
150
+ server_uri_alias: str | None = None
151
+ """Optional URI alias for presentation to the server"""
152
+
153
+ @field_validator("uri", "server_uri_alias")
154
+ @classmethod
155
+ def validate_uri(cls, v: str) -> str:
156
+ """Validate that the URI starts with file:// (required by specification 2024-11-05)"""
157
+ if v and not v.startswith("file://"):
158
+ raise ValueError("Root URI must start with file://")
159
+ return v
160
+
161
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
162
+
163
+
164
+ class MCPServerSettings(BaseModel):
165
+ """
166
+ Represents the configuration for an individual server.
167
+ """
168
+
169
+ name: str | None = None
170
+ """The name of the server."""
171
+
172
+ description: str | None = None
173
+ """The description of the server."""
174
+
175
+ transport: Literal["stdio", "sse", "http"] = "stdio"
176
+ """The transport mechanism."""
177
+
178
+ command: str | None = None
179
+ """The command to execute the server (e.g. npx)."""
180
+
181
+ args: list[str] | None = None
182
+ """The arguments for the server command."""
183
+
184
+ read_timeout_seconds: int | None = None
185
+ """The timeout in seconds for the session."""
186
+
187
+ read_transport_sse_timeout_seconds: int = 300
188
+ """The timeout in seconds for the server connection."""
189
+
190
+ url: str | None = None
191
+ """The URL for the server (e.g. for SSE/SHTTP transport)."""
192
+
193
+ headers: dict[str, str] | None = None
194
+ """Headers dictionary for HTTP connections"""
195
+
196
+ auth: MCPServerAuthSettings | None = None
197
+ """The authentication configuration for the server."""
198
+
199
+ roots: list[MCPRootSettings] | None = None
200
+ """Root directories this server has access to."""
201
+
202
+ env: dict[str, str] | None = None
203
+ """Environment variables to pass to the server process."""
204
+
205
+ sampling: MCPSamplingSettings | None = None
206
+ """Sampling settings for this Client/Server pair"""
207
+
208
+ elicitation: MCPElicitationSettings | None = None
209
+ """Elicitation settings for this Client/Server pair"""
210
+
211
+ cwd: str | None = None
212
+ """Working directory for the executed server command."""
213
+
214
+ load_on_start: bool = True
215
+ """Whether to connect to this server automatically when the agent starts."""
216
+
217
+ include_instructions: bool = True
218
+ """Whether to include this server's instructions in the system prompt (default: True)."""
219
+
220
+ reconnect_on_disconnect: bool = True
221
+ """Whether to automatically reconnect when the server session is terminated (e.g., 404).
222
+
223
+ When enabled, if a remote StreamableHTTP server returns a 404 indicating the session
224
+ has been terminated (e.g., due to server restart), the client will automatically
225
+ attempt to re-initialize the connection and retry the operation.
226
+ """
227
+
228
+ implementation: Implementation | None = None
229
+
230
+ @model_validator(mode="before")
231
+ @classmethod
232
+ def validate_transport_inference(cls, values):
233
+ """Automatically infer transport type based on url/command presence."""
234
+ import warnings
235
+
236
+ if isinstance(values, dict):
237
+ # Check if transport was explicitly provided in the input
238
+ transport_explicit = "transport" in values
239
+ url = values.get("url")
240
+ command = values.get("command")
241
+
242
+ # Only infer if transport was not explicitly set
243
+ if not transport_explicit:
244
+ # Check if we have both url and command specified
245
+ has_url = url is not None and str(url).strip()
246
+ has_command = command is not None and str(command).strip()
247
+
248
+ if has_url and has_command:
249
+ warnings.warn(
250
+ f"MCP Server config has both 'url' ({url}) and 'command' ({command}) specified. "
251
+ "Preferring HTTP transport and ignoring command.",
252
+ UserWarning,
253
+ stacklevel=4,
254
+ )
255
+ values["transport"] = "http"
256
+ values["command"] = None # Clear command to avoid confusion
257
+ elif has_url and not has_command:
258
+ values["transport"] = "http"
259
+ elif has_command and not has_url:
260
+ # Keep default "stdio" for command-based servers
261
+ values["transport"] = "stdio"
262
+ # If neither url nor command is specified, keep default "stdio"
263
+
264
+ return values
265
+
266
+
267
+ class MCPSettings(BaseModel):
268
+ """Configuration for all MCP servers."""
269
+
270
+ servers: dict[str, MCPServerSettings] = {}
271
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
272
+
273
+
274
+ class AnthropicSettings(BaseModel):
275
+ """
276
+ Settings for using Anthropic models in the fast-agent application.
277
+ """
278
+
279
+ api_key: str | None = None
280
+
281
+ base_url: str | None = None
282
+
283
+ cache_mode: Literal["off", "prompt", "auto"] = "auto"
284
+ """
285
+ Controls how caching is applied for Anthropic models when prompt_caching is enabled globally.
286
+ - "off": No caching, even if global prompt_caching is true.
287
+ - "prompt": Caches tools+system prompt (1 block) and template content. Useful for large, static prompts.
288
+ - "auto": Currently same as "prompt" - caches tools+system prompt (1 block) and template content.
289
+ """
290
+
291
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
292
+
293
+
294
+ class OpenAISettings(BaseModel):
295
+ """
296
+ Settings for using OpenAI models in the fast-agent application.
297
+ """
298
+
299
+ api_key: str | None = None
300
+ reasoning_effort: Literal["minimal", "low", "medium", "high"] = "medium"
301
+
302
+ base_url: str | None = None
303
+
304
+ default_headers: dict[str, str] | None = None
305
+ """Custom headers to include in all requests to the OpenAI API."""
306
+
307
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
308
+
309
+
310
+ class DeepSeekSettings(BaseModel):
311
+ """
312
+ Settings for using DeepSeek models in the fast-agent application.
313
+ """
314
+
315
+ api_key: str | None = None
316
+ # reasoning_effort: Literal["low", "medium", "high"] = "medium"
317
+
318
+ base_url: str | None = None
319
+
320
+ default_headers: dict[str, str] | None = None
321
+ """Custom headers to include in all requests to the DeepSeek API."""
322
+
323
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
324
+
325
+
326
+ class GoogleSettings(BaseModel):
327
+ """
328
+ Settings for using Google models (via OpenAI-compatible API) in the fast-agent application.
329
+ """
330
+
331
+ api_key: str | None = None
332
+ # reasoning_effort: Literal["low", "medium", "high"] = "medium"
333
+
334
+ base_url: str | None = None
335
+
336
+ default_headers: dict[str, str] | None = None
337
+ """Custom headers to include in all requests to the Google API."""
338
+
339
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
340
+
341
+
342
+ class XAISettings(BaseModel):
343
+ """
344
+ Settings for using xAI Grok models in the fast-agent application.
345
+ """
346
+
347
+ api_key: str | None = None
348
+ base_url: str | None = "https://api.x.ai/v1"
349
+
350
+ default_headers: dict[str, str] | None = None
351
+ """Custom headers to include in all requests to the xAI API."""
352
+
353
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
354
+
355
+
356
+ class GenericSettings(BaseModel):
357
+ """
358
+ Settings for using generic OpenAI-compatible models in the fast-agent application.
359
+ """
360
+
361
+ api_key: str | None = None
362
+
363
+ base_url: str | None = None
364
+
365
+ default_headers: dict[str, str] | None = None
366
+ """Custom headers to include in all requests to the API."""
367
+
368
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
369
+
370
+
371
+ class OpenRouterSettings(BaseModel):
372
+ """
373
+ Settings for using OpenRouter models via its OpenAI-compatible API.
374
+ """
375
+
376
+ api_key: str | None = None
377
+
378
+ base_url: str | None = None # Optional override, defaults handled in provider
379
+
380
+ default_headers: dict[str, str] | None = None
381
+ """Custom headers to include in all requests to the OpenRouter API."""
382
+
383
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
384
+
385
+
386
+ class AzureSettings(BaseModel):
387
+ """
388
+ Settings for using Azure OpenAI Service in the fast-agent application.
389
+ """
390
+
391
+ api_key: str | None = None
392
+ resource_name: str | None = None
393
+ azure_deployment: str | None = None
394
+ api_version: str | None = None
395
+ base_url: str | None = None # Optional, can be constructed from resource_name
396
+
397
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
398
+
399
+
400
+ class GroqSettings(BaseModel):
401
+ """
402
+ Settings for using Groq models in the fast-agent application.
403
+ """
404
+
405
+ api_key: str | None = None
406
+ base_url: str | None = "https://api.groq.com/openai/v1"
407
+
408
+ default_headers: dict[str, str] | None = None
409
+ """Custom headers to include in all requests to the Groq API."""
410
+
411
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
412
+
413
+
414
+ class OpenTelemetrySettings(BaseModel):
415
+ """
416
+ OTEL settings for the fast-agent application.
417
+ """
418
+
419
+ enabled: bool = False
420
+
421
+ service_name: str = "fast-agent"
422
+
423
+ otlp_endpoint: str = "http://localhost:4318/v1/traces"
424
+ """OTLP endpoint for OpenTelemetry tracing"""
425
+
426
+ console_debug: bool = False
427
+ """Log spans to console"""
428
+
429
+ sample_rate: float = 1.0
430
+ """Sample rate for tracing (1.0 = sample everything)"""
431
+
432
+
433
+ class TensorZeroSettings(BaseModel):
434
+ """
435
+ Settings for using TensorZero via its OpenAI-compatible API.
436
+ """
437
+
438
+ base_url: str | None = None
439
+ api_key: str | None = None
440
+
441
+ default_headers: dict[str, str] | None = None
442
+ """Custom headers to include in all requests to the TensorZero API."""
443
+
444
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
445
+
446
+
447
+ class BedrockSettings(BaseModel):
448
+ """
449
+ Settings for using AWS Bedrock models in the fast-agent application.
450
+ """
451
+
452
+ region: str | None = None
453
+ """AWS region for Bedrock service"""
454
+
455
+ profile: str | None = None
456
+ """AWS profile to use for authentication"""
457
+
458
+ reasoning_effort: Literal["minimal", "low", "medium", "high"] = "minimal"
459
+ """Default reasoning effort for Bedrock models. Can be overridden in model string (e.g., bedrock.claude-sonnet-4-0.high)"""
460
+
461
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
462
+
463
+
464
+ class HuggingFaceSettings(BaseModel):
465
+ """
466
+ Settings for HuggingFace authentication (used for MCP connections).
467
+ """
468
+
469
+ # Leave unset to allow the provider to use its default router endpoint.
470
+ base_url: str | None = None
471
+ api_key: str | None = None
472
+ default_provider: str | None = None
473
+
474
+ default_headers: dict[str, str] | None = None
475
+ """Custom headers to include in all requests to the HuggingFace API."""
476
+
477
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
478
+
479
+
480
+ class LoggerSettings(BaseModel):
481
+ """
482
+ Logger settings for the fast-agent application.
483
+ """
484
+
485
+ type: Literal["none", "console", "file", "http"] = "file"
486
+
487
+ level: Literal["debug", "info", "warning", "error"] = "warning"
488
+ """Minimum logging level"""
489
+
490
+ progress_display: bool = True
491
+ """Enable or disable the progress display"""
492
+
493
+ path: str = "fastagent.jsonl"
494
+ """Path to log file, if logger 'type' is 'file'."""
495
+
496
+ batch_size: int = 100
497
+ """Number of events to accumulate before processing"""
498
+
499
+ flush_interval: float = 2.0
500
+ """How often to flush events in seconds"""
501
+
502
+ max_queue_size: int = 2048
503
+ """Maximum queue size for event processing"""
504
+
505
+ # HTTP transport settings
506
+ http_endpoint: str | None = None
507
+ """HTTP endpoint for event transport"""
508
+
509
+ http_headers: dict[str, str] | None = None
510
+ """HTTP headers for event transport"""
511
+
512
+ http_timeout: float = 5.0
513
+ """HTTP timeout seconds for event transport"""
514
+
515
+ show_chat: bool = True
516
+ """Show chat User/Assistant on the console"""
517
+ show_tools: bool = True
518
+ """Show MCP Sever tool calls on the console"""
519
+ truncate_tools: bool = True
520
+ """Truncate display of long tool calls"""
521
+ enable_markup: bool = True
522
+ """Enable markup in console output. Disable for outputs that may conflict with rich console formatting"""
523
+ streaming: Literal["markdown", "plain", "none"] = "markdown"
524
+ """Streaming renderer for assistant responses"""
525
+
526
+
527
+ def find_fastagent_config_files(start_path: Path) -> tuple[Path | None, Path | None]:
528
+ """
529
+ Find FastAgent configuration files with standardized behavior.
530
+
531
+ Returns:
532
+ Tuple of (config_path, secrets_path) where either can be None if not found.
533
+
534
+ Strategy:
535
+ 1. Find config file recursively from start_path upward
536
+ 2. Prefer secrets file in same directory as config file
537
+ 3. If no secrets file next to config, search recursively from start_path
538
+ """
539
+ config_path = None
540
+ secrets_path = None
541
+
542
+ # First, find the config file with recursive search
543
+ current = start_path.resolve()
544
+ while current != current.parent:
545
+ potential_config = current / "fastagent.config.yaml"
546
+ if potential_config.exists():
547
+ config_path = potential_config
548
+ break
549
+ current = current.parent
550
+
551
+ # If config file found, prefer secrets file in the same directory
552
+ if config_path:
553
+ potential_secrets = config_path.parent / "fastagent.secrets.yaml"
554
+ if potential_secrets.exists():
555
+ secrets_path = potential_secrets
556
+ else:
557
+ # If no secrets file next to config, do recursive search from start
558
+ current = start_path.resolve()
559
+ while current != current.parent:
560
+ potential_secrets = current / "fastagent.secrets.yaml"
561
+ if potential_secrets.exists():
562
+ secrets_path = potential_secrets
563
+ break
564
+ current = current.parent
565
+ else:
566
+ # No config file found, just search for secrets file
567
+ current = start_path.resolve()
568
+ while current != current.parent:
569
+ potential_secrets = current / "fastagent.secrets.yaml"
570
+ if potential_secrets.exists():
571
+ secrets_path = potential_secrets
572
+ break
573
+ current = current.parent
574
+
575
+ return config_path, secrets_path
576
+
577
+
578
+ class Settings(BaseSettings):
579
+ """
580
+ Settings class for the fast-agent application.
581
+ """
582
+
583
+ model_config = SettingsConfigDict(
584
+ env_nested_delimiter="__",
585
+ env_file=".env",
586
+ env_file_encoding="utf-8",
587
+ extra="allow",
588
+ nested_model_default_partial_update=True,
589
+ ) # Customize the behavior of settings here
590
+
591
+ mcp: MCPSettings | None = MCPSettings()
592
+ """MCP config, such as MCP servers"""
593
+
594
+ execution_engine: Literal["asyncio"] = "asyncio"
595
+ """Execution engine for the fast-agent application"""
596
+
597
+ default_model: str | None = None
598
+ """
599
+ Default model for agents. Format is provider.model_name.<reasoning_effort>, for example openai.o3-mini.low
600
+ Aliases are provided for common models e.g. sonnet, haiku, gpt-4.1, o3-mini etc.
601
+ If not set, falls back to FAST_AGENT_MODEL env var, then to "gpt-5-mini.low".
602
+ """
603
+
604
+ auto_sampling: bool = True
605
+ """Enable automatic sampling model selection if not explicitly configured"""
606
+
607
+ anthropic: AnthropicSettings | None = None
608
+ """Settings for using Anthropic models in the fast-agent application"""
609
+
610
+ otel: OpenTelemetrySettings | None = OpenTelemetrySettings()
611
+ """OpenTelemetry logging settings for the fast-agent application"""
612
+
613
+ openai: OpenAISettings | None = None
614
+ """Settings for using OpenAI models in the fast-agent application"""
615
+
616
+ deepseek: DeepSeekSettings | None = None
617
+ """Settings for using DeepSeek models in the fast-agent application"""
618
+
619
+ google: GoogleSettings | None = None
620
+ """Settings for using DeepSeek models in the fast-agent application"""
621
+
622
+ xai: XAISettings | None = None
623
+ """Settings for using xAI Grok models in the fast-agent application"""
624
+
625
+ openrouter: OpenRouterSettings | None = None
626
+ """Settings for using OpenRouter models in the fast-agent application"""
627
+
628
+ generic: GenericSettings | None = None
629
+ """Settings for using Generic models in the fast-agent application"""
630
+
631
+ tensorzero: TensorZeroSettings | None = None
632
+ """Settings for using TensorZero inference gateway"""
633
+
634
+ azure: AzureSettings | None = None
635
+ """Settings for using Azure OpenAI Service in the fast-agent application"""
636
+
637
+ aliyun: OpenAISettings | None = None
638
+ """Settings for using Aliyun OpenAI Service in the fast-agent application"""
639
+
640
+ bedrock: BedrockSettings | None = None
641
+ """Settings for using AWS Bedrock models in the fast-agent application"""
642
+
643
+ hf: HuggingFaceSettings | None = None
644
+ """Settings for HuggingFace authentication (used for MCP connections)"""
645
+
646
+ groq: GroqSettings | None = None
647
+ """Settings for using the Groq provider in the fast-agent application"""
648
+
649
+ logger: LoggerSettings = LoggerSettings()
650
+ """Logger settings for the fast-agent application"""
651
+
652
+ # MCP UI integration mode for handling ui:// embedded resources from MCP tool results
653
+ mcp_ui_mode: Literal["disabled", "enabled", "auto"] = "enabled"
654
+ """Controls handling of MCP UI embedded resources:
655
+ - "disabled": Do not process ui:// resources
656
+ - "enabled": Always extract ui:// resources into message channels (default)
657
+ - "auto": Extract and automatically open ui:// resources.
658
+ """
659
+
660
+ # Output directory for MCP-UI generated HTML files (relative to CWD if not absolute)
661
+ mcp_ui_output_dir: str = ".fast-agent/ui"
662
+ """Directory where MCP-UI HTML files are written. Relative paths are resolved from CWD."""
663
+
664
+ mcp_timeline: MCPTimelineSettings = MCPTimelineSettings()
665
+ """Display settings for MCP activity timelines."""
666
+
667
+ skills: SkillsSettings = SkillsSettings()
668
+ """Local skills discovery and selection settings."""
669
+
670
+ shell_execution: ShellSettings = ShellSettings()
671
+ """Shell execution timeout and warning settings."""
672
+
673
+ llm_retries: int = 0
674
+ """
675
+ Number of times to retry transient LLM API errors.
676
+ Defaults to 0; can be overridden via config or FAST_AGENT_RETRIES env.
677
+ """
678
+
679
+ @classmethod
680
+ def find_config(cls) -> Path | None:
681
+ """Find the config file in the current directory or parent directories."""
682
+ current_dir = Path.cwd()
683
+
684
+ # Check current directory and parent directories
685
+ while current_dir != current_dir.parent:
686
+ for filename in [
687
+ "fastagent.config.yaml",
688
+ ]:
689
+ config_path = current_dir / filename
690
+ if config_path.exists():
691
+ return config_path
692
+ current_dir = current_dir.parent
693
+
694
+ return None
695
+
696
+
697
+ # Global settings object
698
+ _settings: Settings | None = None
699
+
700
+
701
+ def get_settings(config_path: str | os.PathLike[str] | None = None) -> Settings:
702
+ """Get settings instance, automatically loading from config file if available."""
703
+
704
+ def resolve_env_vars(config_item: Any) -> Any:
705
+ """Recursively resolve environment variables in config data."""
706
+ if isinstance(config_item, dict):
707
+ return {k: resolve_env_vars(v) for k, v in config_item.items()}
708
+ elif isinstance(config_item, list):
709
+ return [resolve_env_vars(i) for i in config_item]
710
+ elif isinstance(config_item, str):
711
+ # Regex to find ${ENV_VAR} or ${ENV_VAR:default_value}
712
+ pattern = re.compile(r"\$\{([^}]+)\}")
713
+
714
+ def replace_match(match: re.Match) -> str:
715
+ var_name_with_default = match.group(1)
716
+ if ":" in var_name_with_default:
717
+ var_name, default_value = var_name_with_default.split(":", 1)
718
+ return os.getenv(var_name, default_value)
719
+ else:
720
+ var_name = var_name_with_default
721
+ env_value = os.getenv(var_name)
722
+ if env_value is None:
723
+ # Optionally, raise an error or return the placeholder if the env var is not set
724
+ # For now, returning the placeholder to avoid breaking if not set and no default
725
+ # print(f"Warning: Environment variable {var_name} not set and no default provided.")
726
+ return match.group(0)
727
+ return env_value
728
+
729
+ # Replace all occurrences
730
+ resolved_value = pattern.sub(replace_match, config_item)
731
+ return resolved_value
732
+ return config_item
733
+
734
+ def deep_merge(base: dict, update: dict) -> dict:
735
+ """Recursively merge two dictionaries, preserving nested structures."""
736
+ merged = base.copy()
737
+ for key, value in update.items():
738
+ if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
739
+ merged[key] = deep_merge(merged[key], value)
740
+ else:
741
+ merged[key] = value
742
+ return merged
743
+
744
+ global _settings
745
+
746
+ # If we have a specific config path, always reload settings
747
+ # This ensures each test gets its own config
748
+ if config_path:
749
+ # Reset for the new path
750
+ _settings = None
751
+ elif _settings:
752
+ # Use cached settings only for no specific path
753
+ return _settings
754
+
755
+ # Handle config path - convert string to Path if needed
756
+ config_file: Path | None
757
+ secrets_file: Path | None
758
+ if config_path:
759
+ config_file = Path(config_path)
760
+ # If it's a relative path and doesn't exist, try finding it
761
+ if not config_file.is_absolute() and not config_file.exists():
762
+ # Try resolving against current directory first
763
+ resolved_path = Path.cwd() / config_file.name
764
+ if resolved_path.exists():
765
+ config_file = resolved_path
766
+
767
+ # When config path is explicitly provided, find secrets using standardized logic
768
+ secrets_file = None
769
+ if config_file.exists():
770
+ _, secrets_file = find_fastagent_config_files(config_file.parent)
771
+ else:
772
+ # Use standardized discovery for both config and secrets
773
+ config_file, secrets_file = find_fastagent_config_files(Path.cwd())
774
+
775
+ merged_settings = {}
776
+
777
+ import yaml # pylint: disable=C0415
778
+
779
+ # Load main config if it exists
780
+ if config_file and config_file.exists():
781
+ with open(config_file, "r", encoding="utf-8") as f:
782
+ yaml_settings = yaml.safe_load(f) or {}
783
+ # Resolve environment variables in the loaded YAML settings
784
+ resolved_yaml_settings = resolve_env_vars(yaml_settings)
785
+ merged_settings = resolved_yaml_settings
786
+ elif config_file and not config_file.exists():
787
+ print(f"Warning: Specified config file does not exist: {config_file}")
788
+
789
+ # Load secrets file if found (regardless of whether config file exists)
790
+ if secrets_file and secrets_file.exists():
791
+ with open(secrets_file, "r", encoding="utf-8") as f:
792
+ yaml_secrets = yaml.safe_load(f) or {}
793
+ # Resolve environment variables in the loaded secrets YAML
794
+ resolved_secrets_yaml = resolve_env_vars(yaml_secrets)
795
+ merged_settings = deep_merge(merged_settings, resolved_secrets_yaml)
796
+
797
+ _settings = Settings(**merged_settings)
798
+ return _settings