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,1186 @@
1
+ """
2
+ Direct FastAgent implementation that uses the simplified Agent architecture.
3
+ This replaces the traditional FastAgent with a more streamlined approach that
4
+ directly creates Agent instances without proxies.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import asyncio
11
+ import inspect
12
+ import pathlib
13
+ import sys
14
+ from contextlib import asynccontextmanager
15
+ from dataclasses import dataclass
16
+ from importlib.metadata import version as get_version
17
+ from pathlib import Path
18
+ from typing import (
19
+ TYPE_CHECKING,
20
+ Any,
21
+ AsyncIterator,
22
+ Awaitable,
23
+ Callable,
24
+ Literal,
25
+ ParamSpec,
26
+ TypeVar,
27
+ )
28
+
29
+ import yaml
30
+ from opentelemetry import trace
31
+
32
+ from fast_agent import config
33
+ from fast_agent.core import Core
34
+ from fast_agent.core.agent_app import AgentApp
35
+ from fast_agent.core.direct_decorators import (
36
+ agent as agent_decorator,
37
+ )
38
+ from fast_agent.core.direct_decorators import (
39
+ chain as chain_decorator,
40
+ )
41
+ from fast_agent.core.direct_decorators import (
42
+ custom as custom_decorator,
43
+ )
44
+ from fast_agent.core.direct_decorators import (
45
+ evaluator_optimizer as evaluator_optimizer_decorator,
46
+ )
47
+ from fast_agent.core.direct_decorators import (
48
+ iterative_planner as orchestrator2_decorator,
49
+ )
50
+ from fast_agent.core.direct_decorators import (
51
+ maker as maker_decorator,
52
+ )
53
+ from fast_agent.core.direct_decorators import (
54
+ orchestrator as orchestrator_decorator,
55
+ )
56
+ from fast_agent.core.direct_decorators import (
57
+ parallel as parallel_decorator,
58
+ )
59
+ from fast_agent.core.direct_decorators import (
60
+ router as router_decorator,
61
+ )
62
+ from fast_agent.core.direct_factory import (
63
+ create_agents_in_dependency_order,
64
+ get_default_model_source,
65
+ get_model_factory,
66
+ )
67
+ from fast_agent.core.error_handling import handle_error
68
+ from fast_agent.core.exceptions import (
69
+ AgentConfigError,
70
+ CircularDependencyError,
71
+ ModelConfigError,
72
+ PromptExitError,
73
+ ProviderKeyError,
74
+ ServerConfigError,
75
+ ServerInitializationError,
76
+ )
77
+ from fast_agent.core.logging.logger import get_logger
78
+ from fast_agent.core.prompt_templates import (
79
+ apply_template_variables,
80
+ enrich_with_environment_context,
81
+ )
82
+ from fast_agent.core.validation import (
83
+ validate_provider_keys_post_creation,
84
+ validate_server_references,
85
+ validate_workflow_references,
86
+ )
87
+ from fast_agent.mcp.prompts.prompt_load import load_prompt
88
+ from fast_agent.skills import SkillManifest, SkillRegistry
89
+ from fast_agent.ui.console import configure_console_stream
90
+ from fast_agent.ui.usage_display import display_usage_report
91
+
92
+ if TYPE_CHECKING:
93
+ from mcp.client.session import ElicitationFnT
94
+ from pydantic import AnyUrl
95
+
96
+ from fast_agent.constants import DEFAULT_AGENT_INSTRUCTION
97
+ from fast_agent.context import Context
98
+ from fast_agent.interfaces import AgentProtocol
99
+ from fast_agent.types import PromptMessageExtended
100
+
101
+ F = TypeVar("F", bound=Callable[..., Any]) # For decorated functions
102
+ logger = get_logger(__name__)
103
+
104
+
105
+ class FastAgent:
106
+ """
107
+ A simplified FastAgent implementation that directly creates Agent instances
108
+ without using proxies.
109
+ """
110
+
111
+ def __init__(
112
+ self,
113
+ name: str,
114
+ config_path: str | None = None,
115
+ ignore_unknown_args: bool = False,
116
+ parse_cli_args: bool = True,
117
+ quiet: bool = False, # Add quiet parameter
118
+ skills_directory: str | pathlib.Path | None = None,
119
+ **kwargs,
120
+ ) -> None:
121
+ """
122
+ Initialize the fast-agent application.
123
+
124
+ Args:
125
+ name: Name of the application
126
+ config_path: Optional path to config file
127
+ ignore_unknown_args: Whether to ignore unknown command line arguments
128
+ when parse_cli_args is True.
129
+ parse_cli_args: If True, parse command line arguments using argparse.
130
+ Set to False when embedding FastAgent in another framework
131
+ (like FastAPI/Uvicorn) that handles its own arguments.
132
+ quiet: If True, disable progress display, tool and message logging for cleaner output
133
+ """
134
+
135
+ self.args = argparse.Namespace() # Initialize args always
136
+ self._programmatic_quiet = quiet # Store the programmatic quiet setting
137
+ self._skills_directory_override = (
138
+ Path(skills_directory).expanduser() if skills_directory else None
139
+ )
140
+ self._default_skill_manifests: list[SkillManifest] = []
141
+ self._server_instance_factory = None
142
+ self._server_instance_dispose = None
143
+ self._server_managed_instances: list[AgentInstance] = []
144
+
145
+ # --- Wrap argument parsing logic ---
146
+ if parse_cli_args:
147
+ # Setup command line argument parsing
148
+ parser = argparse.ArgumentParser(description="DirectFastAgent Application")
149
+ parser.add_argument(
150
+ "--model",
151
+ help="Override the default model for all agents",
152
+ )
153
+ parser.add_argument(
154
+ "--agent",
155
+ default="default",
156
+ help="Specify the agent to send a message to (used with --message)",
157
+ )
158
+ parser.add_argument(
159
+ "-m",
160
+ "--message",
161
+ help="Message to send to the specified agent",
162
+ )
163
+ parser.add_argument(
164
+ "-p", "--prompt-file", help="Path to a prompt file to use (either text or JSON)"
165
+ )
166
+ parser.add_argument(
167
+ "--quiet",
168
+ action="store_true",
169
+ help="Disable progress display, tool and message logging for cleaner output",
170
+ )
171
+ parser.add_argument(
172
+ "--version",
173
+ action="store_true",
174
+ help="Show version and exit",
175
+ )
176
+ parser.add_argument(
177
+ "--server",
178
+ action="store_true",
179
+ help="Run as an MCP server",
180
+ )
181
+ parser.add_argument(
182
+ "--transport",
183
+ choices=["sse", "http", "stdio", "acp"],
184
+ default=None,
185
+ help="Transport protocol to use when running as a server (sse, http, stdio, or acp)",
186
+ )
187
+ parser.add_argument(
188
+ "--port",
189
+ type=int,
190
+ default=8000,
191
+ help="Port to use when running as a server with SSE transport",
192
+ )
193
+ parser.add_argument(
194
+ "--host",
195
+ default="0.0.0.0",
196
+ help="Host address to bind to when running as a server with SSE transport",
197
+ )
198
+ parser.add_argument(
199
+ "--instance-scope",
200
+ choices=["shared", "connection", "request"],
201
+ default="shared",
202
+ help="Control MCP agent instancing behaviour (shared, connection, request)",
203
+ )
204
+ parser.add_argument(
205
+ "--skills",
206
+ help="Path to skills directory to use instead of default .claude/skills",
207
+ )
208
+
209
+ if ignore_unknown_args:
210
+ known_args, _ = parser.parse_known_args()
211
+ self.args = known_args
212
+ else:
213
+ # Use parse_known_args here too, to avoid crashing on uvicorn args etc.
214
+ # even if ignore_unknown_args is False, we only care about *our* args.
215
+ known_args, unknown = parser.parse_known_args()
216
+ self.args = known_args
217
+ # Optionally, warn about unknown args if not ignoring?
218
+ # if unknown and not ignore_unknown_args:
219
+ # logger.warning(f"Ignoring unknown command line arguments: {unknown}")
220
+
221
+ # Track whether CLI flags were explicitly provided
222
+ cli_args = sys.argv[1:]
223
+ server_flag_used = "--server" in cli_args
224
+ transport_flag_used = any(
225
+ arg == "--transport" or arg.startswith("--transport=") for arg in cli_args
226
+ )
227
+
228
+ # If a transport was provided, assume server mode even without --server
229
+ if transport_flag_used and not getattr(self.args, "server", False):
230
+ self.args.server = True
231
+
232
+ # Default the transport if still unset
233
+ if getattr(self.args, "transport", None) is None:
234
+ self.args.transport = "http"
235
+
236
+ # Warn that --server is deprecated when the user supplied it explicitly
237
+ if server_flag_used:
238
+ print(
239
+ "--server is deprecated; server mode is implied when --transport is provided. "
240
+ "This flag will be removed in a future release.",
241
+ file=sys.stderr,
242
+ )
243
+
244
+ # Handle version flag
245
+ if self.args.version:
246
+ try:
247
+ app_version = get_version("fast-agent-mcp")
248
+ except: # noqa: E722
249
+ app_version = "unknown"
250
+ print(f"fast-agent-mcp v{app_version}")
251
+ sys.exit(0)
252
+ # --- End of wrapped logic ---
253
+
254
+ # Force quiet mode automatically when running ACP transport
255
+ transport = getattr(self.args, "transport", None)
256
+ if transport == "acp":
257
+ self._programmatic_quiet = True
258
+ setattr(self.args, "quiet", True)
259
+
260
+ # Apply programmatic quiet setting (overrides CLI if both are set)
261
+ if self._programmatic_quiet:
262
+ self.args.quiet = True
263
+
264
+ # Apply CLI skills directory if not already set programmatically
265
+ if (
266
+ self._skills_directory_override is None
267
+ and hasattr(self.args, "skills")
268
+ and self.args.skills
269
+ ):
270
+ self._skills_directory_override = Path(self.args.skills).expanduser()
271
+
272
+ self.name = name
273
+ self.config_path = config_path
274
+
275
+ try:
276
+ # Load configuration directly for this instance
277
+ self._load_config()
278
+
279
+ # Apply programmatic quiet mode to config before creating app
280
+ if self._programmatic_quiet and hasattr(self, "config"):
281
+ if "logger" not in self.config:
282
+ self.config["logger"] = {}
283
+ self.config["logger"]["progress_display"] = False
284
+ self.config["logger"]["show_chat"] = False
285
+ self.config["logger"]["show_tools"] = False
286
+
287
+ # Create the app with our local settings
288
+ self.app = Core(
289
+ name=name,
290
+ settings=config.Settings(**self.config) if hasattr(self, "config") else None,
291
+ **kwargs,
292
+ )
293
+
294
+ # Stop progress display immediately if quiet mode is requested
295
+ if self._programmatic_quiet:
296
+ from fast_agent.ui.progress_display import progress_display
297
+
298
+ progress_display.stop()
299
+
300
+ except yaml.parser.ParserError as e:
301
+ handle_error(
302
+ e,
303
+ "YAML Parsing Error",
304
+ "There was an error parsing the config or secrets YAML configuration file.",
305
+ )
306
+ raise SystemExit(1)
307
+
308
+ # Dictionary to store agent configurations from decorators
309
+ self.agents: dict[str, dict[str, Any]] = {}
310
+
311
+ def _load_config(self) -> None:
312
+ """Load configuration from YAML file including secrets using get_settings
313
+ but without relying on the global cache."""
314
+
315
+ # Import but make a local copy to avoid affecting the global state
316
+ from fast_agent.config import _settings, get_settings
317
+
318
+ # Temporarily clear the global settings to ensure a fresh load
319
+ old_settings = _settings
320
+ _settings = None
321
+
322
+ try:
323
+ # Use get_settings to load config - this handles all paths and secrets merging
324
+ settings = get_settings(self.config_path)
325
+
326
+ # Convert to dict for backward compatibility
327
+ self.config = settings.model_dump() if settings else {}
328
+ finally:
329
+ # Restore the original global settings
330
+ _settings = old_settings
331
+
332
+ @property
333
+ def context(self) -> Context:
334
+ """Access the application context"""
335
+ return self.app.context
336
+
337
+ # Decorator methods with precise signatures for IDE completion
338
+ # Provide annotations so IDEs can discover these attributes on instances
339
+ if TYPE_CHECKING: # pragma: no cover - typing aid only
340
+ from collections.abc import Coroutine
341
+ from pathlib import Path
342
+
343
+ from fast_agent.skills import SkillManifest, SkillRegistry
344
+ from fast_agent.types import RequestParams
345
+
346
+ P = ParamSpec("P")
347
+ R = TypeVar("R")
348
+
349
+ def agent(
350
+ self,
351
+ name: str = "default",
352
+ instruction_or_kwarg: str | Path | AnyUrl | None = None,
353
+ *,
354
+ instruction: str | Path | AnyUrl = DEFAULT_AGENT_INSTRUCTION,
355
+ agents: list[str] | None = None,
356
+ servers: list[str] = [],
357
+ tools: dict[str, list[str]] | None = None,
358
+ resources: dict[str, list[str]] | None = None,
359
+ prompts: dict[str, list[str]] | None = None,
360
+ skills: list[SkillManifest | SkillRegistry | Path | str | None] | None = None,
361
+ model: str | None = None,
362
+ use_history: bool = True,
363
+ request_params: RequestParams | None = None,
364
+ human_input: bool = False,
365
+ default: bool = False,
366
+ elicitation_handler: ElicitationFnT | None = None,
367
+ api_key: str | None = None,
368
+ history_mode: Any | None = None,
369
+ max_parallel: int | None = None,
370
+ child_timeout_sec: int | None = None,
371
+ max_display_instances: int | None = None,
372
+ ) -> Callable[
373
+ [Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]
374
+ ]: ...
375
+
376
+ def custom(
377
+ self,
378
+ cls,
379
+ name: str = "default",
380
+ instruction_or_kwarg: str | Path | AnyUrl | None = None,
381
+ *,
382
+ instruction: str | Path | AnyUrl = DEFAULT_AGENT_INSTRUCTION,
383
+ servers: list[str] = [],
384
+ tools: dict[str, list[str]] | None = None,
385
+ resources: dict[str, list[str]] | None = None,
386
+ prompts: dict[str, list[str]] | None = None,
387
+ model: str | None = None,
388
+ use_history: bool = True,
389
+ request_params: RequestParams | None = None,
390
+ human_input: bool = False,
391
+ default: bool = False,
392
+ elicitation_handler: ElicitationFnT | None = None,
393
+ api_key: str | None = None,
394
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]: ...
395
+
396
+ def orchestrator(
397
+ self,
398
+ name: str,
399
+ *,
400
+ agents: list[str],
401
+ instruction: str
402
+ | Path
403
+ | AnyUrl = "You are an expert planner. Given an objective task and a list of Agents\n(which are collections of capabilities), your job is to break down the objective\ninto a series of steps, which can be performed by these agents.\n",
404
+ model: str | None = None,
405
+ request_params: RequestParams | None = None,
406
+ use_history: bool = False,
407
+ human_input: bool = False,
408
+ plan_type: Literal["full", "iterative"] = "full",
409
+ plan_iterations: int = 5,
410
+ default: bool = False,
411
+ api_key: str | None = None,
412
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]: ...
413
+
414
+ def iterative_planner(
415
+ self,
416
+ name: str,
417
+ *,
418
+ agents: list[str],
419
+ instruction: str | Path | AnyUrl = "You are an expert planner. Plan iteratively.",
420
+ model: str | None = None,
421
+ request_params: RequestParams | None = None,
422
+ plan_iterations: int = -1,
423
+ default: bool = False,
424
+ api_key: str | None = None,
425
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]: ...
426
+
427
+ def router(
428
+ self,
429
+ name: str,
430
+ *,
431
+ agents: list[str],
432
+ instruction: str | Path | AnyUrl | None = None,
433
+ servers: list[str] = [],
434
+ tools: dict[str, list[str]] | None = None,
435
+ resources: dict[str, list[str]] | None = None,
436
+ prompts: dict[str, list[str]] | None = None,
437
+ model: str | None = None,
438
+ use_history: bool = False,
439
+ request_params: RequestParams | None = None,
440
+ human_input: bool = False,
441
+ default: bool = False,
442
+ elicitation_handler: ElicitationFnT | None = None,
443
+ api_key: str | None = None,
444
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]: ...
445
+
446
+ def chain(
447
+ self,
448
+ name: str,
449
+ *,
450
+ sequence: list[str],
451
+ instruction: str | Path | AnyUrl | None = None,
452
+ cumulative: bool = False,
453
+ default: bool = False,
454
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]: ...
455
+
456
+ def parallel(
457
+ self,
458
+ name: str,
459
+ *,
460
+ fan_out: list[str],
461
+ fan_in: str | None = None,
462
+ instruction: str | Path | AnyUrl | None = None,
463
+ include_request: bool = True,
464
+ default: bool = False,
465
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]: ...
466
+
467
+ def evaluator_optimizer(
468
+ self,
469
+ name: str,
470
+ *,
471
+ generator: str,
472
+ evaluator: str,
473
+ instruction: str | Path | AnyUrl | None = None,
474
+ min_rating: str = "GOOD",
475
+ max_refinements: int = 3,
476
+ refinement_instruction: str | None = None,
477
+ default: bool = False,
478
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]: ...
479
+
480
+ def maker(
481
+ self,
482
+ name: str,
483
+ *,
484
+ worker: str,
485
+ k: int = 3,
486
+ max_samples: int = 50,
487
+ match_strategy: str = "exact",
488
+ red_flag_max_length: int | None = None,
489
+ instruction: str | Path | AnyUrl | None = None,
490
+ default: bool = False,
491
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]: ...
492
+
493
+ # Runtime bindings (actual implementations)
494
+ agent = agent_decorator
495
+ custom = custom_decorator
496
+ orchestrator = orchestrator_decorator
497
+ iterative_planner = orchestrator2_decorator
498
+ router = router_decorator
499
+ chain = chain_decorator
500
+ parallel = parallel_decorator
501
+ evaluator_optimizer = evaluator_optimizer_decorator
502
+ maker = maker_decorator
503
+
504
+ def _get_acp_server_class(self):
505
+ """Import and return the ACP server class with helpful error handling."""
506
+ try:
507
+ from fast_agent.acp.server import AgentACPServer
508
+
509
+ return AgentACPServer
510
+ except ModuleNotFoundError as exc:
511
+ if exc.name == "acp":
512
+ raise ServerInitializationError(
513
+ "ACP transport requires the 'agent-client-protocol' package. "
514
+ "Install it via `pip install fast-agent-mcp[acp]` or "
515
+ "`pip install agent-client-protocol`."
516
+ ) from exc
517
+ raise
518
+
519
+ @asynccontextmanager
520
+ async def run(self) -> AsyncIterator["AgentApp"]:
521
+ """
522
+ Context manager for running the application.
523
+ Initializes all registered agents.
524
+ """
525
+ active_agents: dict[str, AgentProtocol] = {}
526
+ had_error = False
527
+ await self.app.initialize()
528
+
529
+ # Handle quiet mode and CLI model override safely
530
+ # Define these *before* they are used, checking if self.args exists and has the attributes
531
+ # Force quiet mode for stdio/acp transports to avoid polluting the protocol stream
532
+ quiet_mode = getattr(self.args, "quiet", False)
533
+ if getattr(self.args, "transport", None) in ["stdio", "acp"] and getattr(
534
+ self.args, "server", False
535
+ ):
536
+ quiet_mode = True
537
+ cli_model_override = getattr(self.args, "model", None)
538
+
539
+ # Store the model source for UI display
540
+ model_source = get_default_model_source(
541
+ config_default_model=self.context.config.default_model,
542
+ cli_model=cli_model_override,
543
+ )
544
+ if self.context.config:
545
+ self.context.config.model_source = model_source # type: ignore[attr-defined]
546
+
547
+ tracer = trace.get_tracer(__name__)
548
+ with tracer.start_as_current_span(self.name):
549
+ try:
550
+ async with self.app.run():
551
+ registry = getattr(self.context, "skill_registry", None)
552
+ if self._skills_directory_override is not None:
553
+ override_registry = SkillRegistry(
554
+ base_dir=Path.cwd(),
555
+ override_directory=self._skills_directory_override,
556
+ )
557
+ self.context.skill_registry = override_registry
558
+ registry = override_registry
559
+
560
+ default_skills: list[SkillManifest] = []
561
+ if registry:
562
+ default_skills = registry.load_manifests()
563
+
564
+ self._apply_skills_to_agent_configs(default_skills)
565
+
566
+ # Apply quiet mode if requested
567
+ if quiet_mode:
568
+ cfg = self.app.context.config
569
+ if cfg is not None and cfg.logger is not None:
570
+ # Update our app's config directly
571
+ cfg_logger = cfg.logger
572
+ cfg_logger.progress_display = False
573
+ cfg_logger.show_chat = False
574
+ cfg_logger.show_tools = False
575
+
576
+ # Directly disable the progress display singleton
577
+ from fast_agent.ui.progress_display import progress_display
578
+
579
+ progress_display.stop()
580
+
581
+ # Pre-flight validation
582
+ if 0 == len(self.agents):
583
+ raise AgentConfigError(
584
+ "No agents defined. Please define at least one agent."
585
+ )
586
+ validate_server_references(self.context, self.agents)
587
+ validate_workflow_references(self.agents)
588
+
589
+ # Get a model factory function
590
+ # Now cli_model_override is guaranteed to be defined
591
+ def model_factory_func(model=None, request_params=None):
592
+ return get_model_factory(
593
+ self.context,
594
+ model=model,
595
+ request_params=request_params,
596
+ cli_model=cli_model_override, # Use the variable defined above
597
+ )
598
+
599
+ managed_instances: list[AgentInstance] = []
600
+ instance_lock = asyncio.Lock()
601
+
602
+ # Determine whether to apply global environment template variables.
603
+ apply_global_prompt_context = not (
604
+ getattr(self.args, "server", False)
605
+ and getattr(self.args, "transport", None) == "acp"
606
+ )
607
+ global_prompt_context: dict[str, str] | None = None
608
+ if apply_global_prompt_context:
609
+ context_variables: dict[str, str] = {}
610
+ client_info: dict[str, str] = {"name": self.name}
611
+ cli_name = getattr(self.args, "name", None)
612
+ if cli_name:
613
+ client_info["title"] = cli_name
614
+
615
+ # Pass skills directory override if configured
616
+ skills_override = (
617
+ str(self._skills_directory_override)
618
+ if self._skills_directory_override
619
+ else None
620
+ )
621
+
622
+ enrich_with_environment_context(
623
+ context_variables, str(Path.cwd()), client_info, skills_override
624
+ )
625
+ if context_variables:
626
+ global_prompt_context = context_variables
627
+
628
+ async def instantiate_agent_instance() -> AgentInstance:
629
+ async with instance_lock:
630
+ agents_map = await create_agents_in_dependency_order(
631
+ self.app,
632
+ self.agents,
633
+ model_factory_func,
634
+ )
635
+ validate_provider_keys_post_creation(agents_map)
636
+ instance = AgentInstance(AgentApp(agents_map), agents_map)
637
+ managed_instances.append(instance)
638
+ if global_prompt_context:
639
+ self._apply_instruction_context(instance, global_prompt_context)
640
+ return instance
641
+
642
+ async def dispose_agent_instance(instance: AgentInstance) -> None:
643
+ async with instance_lock:
644
+ if instance in managed_instances:
645
+ managed_instances.remove(instance)
646
+ await instance.shutdown()
647
+
648
+ primary_instance = await instantiate_agent_instance()
649
+ wrapper = primary_instance.app
650
+ active_agents = primary_instance.agents
651
+
652
+ self._server_instance_factory = instantiate_agent_instance
653
+ self._server_instance_dispose = dispose_agent_instance
654
+ self._server_managed_instances = managed_instances
655
+
656
+ # Disable streaming if parallel agents are present
657
+ from fast_agent.agents.agent_types import AgentType
658
+
659
+ has_parallel = any(
660
+ agent.agent_type == AgentType.PARALLEL for agent in active_agents.values()
661
+ )
662
+ if has_parallel:
663
+ cfg = self.app.context.config
664
+ if cfg is not None and cfg.logger is not None:
665
+ cfg.logger.streaming = "none"
666
+
667
+ # Handle command line options that should be processed after agent initialization
668
+
669
+ # Handle --server option
670
+ # Check if parse_cli_args was True before checking self.args.server
671
+ if hasattr(self.args, "server") and self.args.server:
672
+ # For stdio/acp transports, use stderr to avoid interfering with JSON-RPC
673
+ import sys
674
+
675
+ is_stdio_transport = self.args.transport in ["stdio", "acp"]
676
+ configure_console_stream("stderr" if is_stdio_transport else "stdout")
677
+ output_stream = sys.stderr if is_stdio_transport else sys.stdout
678
+
679
+ try:
680
+ # Print info message if not in quiet mode
681
+ if not quiet_mode:
682
+ print(
683
+ f"Starting fast-agent '{self.name}' in server mode",
684
+ file=output_stream,
685
+ )
686
+ print(f"Transport: {self.args.transport}", file=output_stream)
687
+ if self.args.transport == "sse":
688
+ print(
689
+ f"Listening on {self.args.host}:{self.args.port}",
690
+ file=output_stream,
691
+ )
692
+ print("Press Ctrl+C to stop", file=output_stream)
693
+
694
+ # Check which server type to use based on transport
695
+ if self.args.transport == "acp":
696
+ # Create and run ACP server
697
+ AgentACPServer = self._get_acp_server_class()
698
+
699
+ server_name = getattr(self.args, "server_name", None)
700
+ instance_scope = getattr(self.args, "instance_scope", "shared")
701
+ permissions_enabled = getattr(
702
+ self.args, "permissions_enabled", True
703
+ )
704
+
705
+ # Pass skills directory override if configured
706
+ skills_override = (
707
+ str(self._skills_directory_override)
708
+ if self._skills_directory_override
709
+ else None
710
+ )
711
+
712
+ acp_server = AgentACPServer(
713
+ primary_instance=primary_instance,
714
+ create_instance=self._server_instance_factory,
715
+ dispose_instance=self._server_instance_dispose,
716
+ instance_scope=instance_scope,
717
+ server_name=server_name or f"{self.name}",
718
+ skills_directory_override=skills_override,
719
+ permissions_enabled=permissions_enabled,
720
+ )
721
+
722
+ # Run the ACP server (this is a blocking call)
723
+ await acp_server.run_async()
724
+ else:
725
+ # Create the MCP server
726
+ from fast_agent.mcp.server import AgentMCPServer
727
+
728
+ tool_description = getattr(self.args, "tool_description", None)
729
+ server_description = getattr(self.args, "server_description", None)
730
+ server_name = getattr(self.args, "server_name", None)
731
+ instance_scope = getattr(self.args, "instance_scope", "shared")
732
+ mcp_server = AgentMCPServer(
733
+ primary_instance=primary_instance,
734
+ create_instance=self._server_instance_factory,
735
+ dispose_instance=self._server_instance_dispose,
736
+ instance_scope=instance_scope,
737
+ server_name=server_name or f"{self.name}-MCP-Server",
738
+ server_description=server_description,
739
+ tool_description=tool_description,
740
+ )
741
+
742
+ # Run the server directly (this is a blocking call)
743
+ await mcp_server.run_async(
744
+ transport=self.args.transport,
745
+ host=self.args.host,
746
+ port=self.args.port,
747
+ )
748
+ except KeyboardInterrupt:
749
+ if not quiet_mode:
750
+ print("\nServer stopped by user (Ctrl+C)", file=output_stream)
751
+ raise SystemExit(0)
752
+ except Exception as e:
753
+ if not quiet_mode:
754
+ import traceback
755
+
756
+ traceback.print_exc()
757
+ print(f"\nServer stopped with error: {e}", file=output_stream)
758
+ else:
759
+ print(f"\nServer stopped with error: {e}", file=output_stream)
760
+ raise SystemExit(1)
761
+
762
+ # Exit after server shutdown
763
+ raise SystemExit(0)
764
+
765
+ # Handle direct message sending if --message is provided
766
+ if hasattr(self.args, "message") and self.args.message:
767
+ agent_name = self.args.agent
768
+ message = self.args.message
769
+
770
+ if agent_name not in active_agents:
771
+ available_agents = ", ".join(active_agents.keys())
772
+ print(
773
+ f"\n\nError: Agent '{agent_name}' not found. Available agents: {available_agents}"
774
+ )
775
+ raise SystemExit(1)
776
+
777
+ try:
778
+ # Get response from the agent
779
+ agent = active_agents[agent_name]
780
+ response = await agent.send(message)
781
+
782
+ # In quiet mode, just print the raw response
783
+ # The chat display should already be turned off by the configuration
784
+ if self.args.quiet:
785
+ print(f"{response}")
786
+
787
+ raise SystemExit(0)
788
+ except Exception as e:
789
+ print(f"\n\nError sending message to agent '{agent_name}': {str(e)}")
790
+ raise SystemExit(1)
791
+
792
+ if hasattr(self.args, "prompt_file") and self.args.prompt_file:
793
+ agent_name = self.args.agent
794
+ prompt: list[PromptMessageExtended] = load_prompt(
795
+ Path(self.args.prompt_file)
796
+ )
797
+ if agent_name not in active_agents:
798
+ available_agents = ", ".join(active_agents.keys())
799
+ print(
800
+ f"\n\nError: Agent '{agent_name}' not found. Available agents: {available_agents}"
801
+ )
802
+ raise SystemExit(1)
803
+
804
+ try:
805
+ # Get response from the agent
806
+ agent = active_agents[agent_name]
807
+ prompt_result = await agent.generate(prompt)
808
+
809
+ # In quiet mode, just print the raw response
810
+ # The chat display should already be turned off by the configuration
811
+ if self.args.quiet:
812
+ print(f"{prompt_result.last_text()}")
813
+
814
+ raise SystemExit(0)
815
+ except Exception as e:
816
+ print(f"\n\nError sending message to agent '{agent_name}': {str(e)}")
817
+ raise SystemExit(1)
818
+
819
+ yield wrapper
820
+
821
+ except PromptExitError as e:
822
+ # User requested exit - not an error, show usage report
823
+ self._handle_error(e)
824
+ raise SystemExit(0)
825
+ except (
826
+ ServerConfigError,
827
+ ProviderKeyError,
828
+ AgentConfigError,
829
+ ServerInitializationError,
830
+ ModelConfigError,
831
+ CircularDependencyError,
832
+ ) as e:
833
+ had_error = True
834
+ self._handle_error(e)
835
+ raise SystemExit(1)
836
+
837
+ finally:
838
+ # Ensure progress display is stopped before showing usage summary
839
+ try:
840
+ from fast_agent.ui.progress_display import progress_display
841
+
842
+ progress_display.stop()
843
+ except: # noqa: E722
844
+ pass
845
+
846
+ # Print usage report before cleanup (show for user exits too)
847
+ if (
848
+ getattr(self, "_server_managed_instances", None)
849
+ and not had_error
850
+ and not quiet_mode
851
+ and getattr(self.args, "server", False) is False
852
+ ):
853
+ # Only show usage report for non-server interactive runs
854
+ if managed_instances:
855
+ instance = managed_instances[0]
856
+ self._print_usage_report(instance.agents)
857
+ elif active_agents and not had_error and not quiet_mode:
858
+ self._print_usage_report(active_agents)
859
+
860
+ # Clean up any active agents (always cleanup, even on errors)
861
+ if getattr(self, "_server_managed_instances", None) and getattr(
862
+ self, "_server_instance_dispose", None
863
+ ):
864
+ # Dispose any remaining instances
865
+ remaining_instances = list(self._server_managed_instances)
866
+ for instance in remaining_instances:
867
+ try:
868
+ await self._server_instance_dispose(instance)
869
+ except Exception:
870
+ pass
871
+ self._server_managed_instances.clear()
872
+ elif active_agents:
873
+ for agent in active_agents.values():
874
+ try:
875
+ await agent.shutdown()
876
+ except Exception:
877
+ pass
878
+
879
+ def _apply_instruction_context(
880
+ self, instance: AgentInstance, context_vars: dict[str, str]
881
+ ) -> None:
882
+ """Resolve late-binding placeholders for all agents in the provided instance."""
883
+ if not context_vars:
884
+ return
885
+
886
+ for agent in instance.agents.values():
887
+ template = getattr(agent, "instruction", None)
888
+ if not template:
889
+ continue
890
+
891
+ resolved = apply_template_variables(template, context_vars)
892
+ if resolved == template:
893
+ continue
894
+
895
+ agent.instruction = resolved
896
+
897
+ # Note: We intentionally do NOT modify config.instruction here.
898
+ # The config should preserve the original template so that
899
+ # downstream logic (like MCP display) can check for template
900
+ # variables like {{serverInstructions}}.
901
+
902
+ request_params = getattr(agent, "_default_request_params", None)
903
+ if request_params is not None:
904
+ request_params.systemPrompt = resolved
905
+
906
+ # TODO -- find a cleaner way of doing this
907
+ # Keep any attached LLM in sync so the provider sees the resolved prompt
908
+ llm = getattr(agent, "_llm", None)
909
+ if llm is not None:
910
+ if getattr(llm, "default_request_params", None) is not None:
911
+ llm.default_request_params.systemPrompt = resolved
912
+ if hasattr(llm, "instruction"):
913
+ llm.instruction = resolved
914
+
915
+ def _apply_skills_to_agent_configs(self, default_skills: list[SkillManifest]) -> None:
916
+ self._default_skill_manifests = list(default_skills)
917
+
918
+ for agent_data in self.agents.values():
919
+ config_obj = agent_data.get("config")
920
+ if not config_obj:
921
+ continue
922
+
923
+ resolved = self._resolve_skills(config_obj.skills)
924
+ if not resolved:
925
+ resolved = list(default_skills)
926
+ else:
927
+ resolved = self._deduplicate_skills(resolved)
928
+
929
+ config_obj.skill_manifests = resolved
930
+
931
+ def _resolve_skills(
932
+ self,
933
+ entry: SkillManifest
934
+ | SkillRegistry
935
+ | Path
936
+ | str
937
+ | list[SkillManifest | SkillRegistry | Path | str | None]
938
+ | None,
939
+ ) -> list[SkillManifest]:
940
+ if entry is None:
941
+ return []
942
+ if isinstance(entry, list):
943
+ manifests: list[SkillManifest] = []
944
+ for item in entry:
945
+ manifests.extend(self._resolve_skills(item))
946
+ return manifests
947
+ if isinstance(entry, SkillManifest):
948
+ return [entry]
949
+ if isinstance(entry, SkillRegistry):
950
+ try:
951
+ return entry.load_manifests()
952
+ except Exception:
953
+ logger.debug(
954
+ "Failed to load skills from registry",
955
+ data={"registry": type(entry).__name__},
956
+ )
957
+ return []
958
+ if isinstance(entry, (Path, str)):
959
+ # Use instance method to preserve original path for relative path computation
960
+ path = Path(entry) if isinstance(entry, str) else entry
961
+ registry = SkillRegistry(base_dir=Path.cwd(), override_directory=path)
962
+ return registry.load_manifests()
963
+
964
+ logger.debug(
965
+ "Unsupported skill entry type",
966
+ data={"type": type(entry).__name__},
967
+ )
968
+ return []
969
+
970
+ @staticmethod
971
+ def _deduplicate_skills(manifests: list[SkillManifest]) -> list[SkillManifest]:
972
+ unique: dict[str, SkillManifest] = {}
973
+ for manifest in manifests:
974
+ key = manifest.name.lower()
975
+ if key not in unique:
976
+ unique[key] = manifest
977
+ return list(unique.values())
978
+
979
+ def _handle_error(self, e: Exception, error_type: str | None = None) -> None:
980
+ """
981
+ Handle errors with consistent formatting and messaging.
982
+
983
+ Args:
984
+ e: The exception that was raised
985
+ error_type: Optional explicit error type
986
+ """
987
+ if isinstance(e, ServerConfigError):
988
+ handle_error(
989
+ e,
990
+ "Server Configuration Error",
991
+ "Please check your 'fastagent.config.yaml' configuration file and add the missing server definitions.",
992
+ )
993
+ elif isinstance(e, ProviderKeyError):
994
+ handle_error(
995
+ e,
996
+ "Provider Configuration Error",
997
+ "Please check your 'fastagent.secrets.yaml' configuration file and ensure all required API keys are set.",
998
+ )
999
+ elif isinstance(e, AgentConfigError):
1000
+ handle_error(
1001
+ e,
1002
+ "Workflow or Agent Configuration Error",
1003
+ "Please check your agent definition and ensure names and references are correct.",
1004
+ )
1005
+ elif isinstance(e, ServerInitializationError):
1006
+ handle_error(
1007
+ e,
1008
+ "MCP Server Startup Error",
1009
+ "There was an error starting up the MCP Server.",
1010
+ )
1011
+ elif isinstance(e, ModelConfigError):
1012
+ handle_error(
1013
+ e,
1014
+ "Model Configuration Error",
1015
+ "Common models: gpt-5.1, kimi, sonnet, haiku. Set reasoning effort on supported models with gpt-5-mini.high",
1016
+ )
1017
+ elif isinstance(e, CircularDependencyError):
1018
+ handle_error(
1019
+ e,
1020
+ "Circular Dependency Detected",
1021
+ "Check your agent configuration for circular dependencies.",
1022
+ )
1023
+ elif isinstance(e, PromptExitError):
1024
+ handle_error(
1025
+ e,
1026
+ "User requested exit",
1027
+ )
1028
+ elif isinstance(e, asyncio.CancelledError):
1029
+ handle_error(
1030
+ e,
1031
+ "Cancelled",
1032
+ "The operation was cancelled.",
1033
+ )
1034
+ else:
1035
+ handle_error(e, error_type or "Error", "An unexpected error occurred.")
1036
+
1037
+ def _print_usage_report(self, active_agents: dict) -> None:
1038
+ """Print a formatted table of token usage for all agents."""
1039
+ display_usage_report(active_agents, show_if_progress_disabled=False, subdued_colors=True)
1040
+
1041
+ async def start_server(
1042
+ self,
1043
+ transport: str = "http",
1044
+ host: str = "0.0.0.0",
1045
+ port: int = 8000,
1046
+ server_name: str | None = None,
1047
+ server_description: str | None = None,
1048
+ tool_description: str | None = None,
1049
+ instance_scope: str = "shared",
1050
+ permissions_enabled: bool = True,
1051
+ ) -> None:
1052
+ """
1053
+ Start the application as an MCP server.
1054
+ This method initializes agents and exposes them through an MCP server.
1055
+ It is a blocking method that runs until the server is stopped.
1056
+
1057
+ Args:
1058
+ transport: Transport protocol to use ("stdio" or "sse")
1059
+ host: Host address for the server when using SSE
1060
+ port: Port for the server when using SSE
1061
+ server_name: Optional custom name for the MCP server
1062
+ server_description: Optional description/instructions for the MCP server
1063
+ tool_description: Optional description template for the exposed send tool.
1064
+ Use {agent} to reference the agent name.
1065
+ permissions_enabled: Whether to request tool permissions from ACP clients (default: True)
1066
+ """
1067
+ # This method simply updates the command line arguments and uses run()
1068
+ # to ensure we follow the same initialization path for all operations
1069
+
1070
+ # Store original args
1071
+ original_args = None
1072
+ if hasattr(self, "args"):
1073
+ original_args = self.args
1074
+
1075
+ # Create our own args object with server settings
1076
+ from argparse import Namespace
1077
+
1078
+ self.args = Namespace()
1079
+ self.args.server = True
1080
+ self.args.transport = transport
1081
+ self.args.host = host
1082
+ self.args.port = port
1083
+ self.args.tool_description = tool_description
1084
+ self.args.server_description = server_description
1085
+ self.args.server_name = server_name
1086
+ self.args.instance_scope = instance_scope
1087
+ self.args.permissions_enabled = permissions_enabled
1088
+ # Force quiet mode for stdio/acp transports to avoid polluting the protocol stream
1089
+ self.args.quiet = (
1090
+ original_args.quiet if original_args and hasattr(original_args, "quiet") else False
1091
+ )
1092
+ if transport in ["stdio", "acp"]:
1093
+ self.args.quiet = True
1094
+ self.args.model = None
1095
+ if original_args is not None and hasattr(original_args, "model"):
1096
+ self.args.model = original_args.model
1097
+
1098
+ # Run the application, which will detect the server flag and start server mode
1099
+ async with self.run():
1100
+ pass # This won't be reached due to SystemExit in run()
1101
+
1102
+ # Restore original args (if we get here)
1103
+ if original_args:
1104
+ self.args = original_args
1105
+
1106
+ # Keep run_with_mcp_server for backward compatibility
1107
+ async def run_with_mcp_server(
1108
+ self,
1109
+ transport: str = "sse",
1110
+ host: str = "0.0.0.0",
1111
+ port: int = 8000,
1112
+ server_name: str | None = None,
1113
+ server_description: str | None = None,
1114
+ tool_description: str | None = None,
1115
+ instance_scope: str = "shared",
1116
+ ) -> None:
1117
+ """
1118
+ Run the application and expose agents through an MCP server.
1119
+ This method is kept for backward compatibility.
1120
+ For new code, use start_server() instead.
1121
+
1122
+ Args:
1123
+ transport: Transport protocol to use ("stdio" or "sse")
1124
+ host: Host address for the server when using SSE
1125
+ port: Port for the server when using SSE
1126
+ server_name: Optional custom name for the MCP server
1127
+ server_description: Optional description/instructions for the MCP server
1128
+ tool_description: Optional description template for the exposed send tool.
1129
+ """
1130
+ await self.start_server(
1131
+ transport=transport,
1132
+ host=host,
1133
+ port=port,
1134
+ server_name=server_name,
1135
+ server_description=server_description,
1136
+ tool_description=tool_description,
1137
+ instance_scope=instance_scope,
1138
+ )
1139
+
1140
+ async def main(self):
1141
+ """
1142
+ Helper method for checking if server mode was requested.
1143
+
1144
+ Usage:
1145
+ ```python
1146
+ fast = FastAgent("My App")
1147
+
1148
+ @fast.agent(...)
1149
+ async def app_main():
1150
+ # Check if server mode was requested
1151
+ # This doesn't actually do anything - the check happens in run()
1152
+ # But it provides a way for application code to know if server mode
1153
+ # was requested for conditionals
1154
+ is_server_mode = hasattr(self, "args") and self.args.server
1155
+
1156
+ # Normal run - this will handle server mode automatically if requested
1157
+ async with fast.run() as agent:
1158
+ # This code only executes for normal mode
1159
+ # Server mode will exit before reaching here
1160
+ await agent.send("Hello")
1161
+ ```
1162
+
1163
+ Returns:
1164
+ bool: True if --server flag is set, False otherwise
1165
+ """
1166
+ # Just check if the flag is set, no action here
1167
+ # The actual server code will be handled by run()
1168
+ return hasattr(self, "args") and self.args.server
1169
+
1170
+
1171
+ @dataclass
1172
+ class AgentInstance:
1173
+ app: AgentApp
1174
+ agents: dict[str, "AgentProtocol"]
1175
+
1176
+ async def shutdown(self) -> None:
1177
+ for agent in self.agents.values():
1178
+ try:
1179
+ shutdown = getattr(agent, "shutdown", None)
1180
+ if shutdown is None:
1181
+ continue
1182
+ result = shutdown()
1183
+ if inspect.isawaitable(result):
1184
+ await result
1185
+ except Exception:
1186
+ pass