fast-agent-mcp 0.2.58__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

Files changed (233) hide show
  1. fast_agent/__init__.py +127 -0
  2. fast_agent/agents/__init__.py +36 -0
  3. {mcp_agent/core → fast_agent/agents}/agent_types.py +2 -1
  4. fast_agent/agents/llm_agent.py +217 -0
  5. fast_agent/agents/llm_decorator.py +486 -0
  6. mcp_agent/agents/base_agent.py → fast_agent/agents/mcp_agent.py +377 -385
  7. fast_agent/agents/tool_agent.py +168 -0
  8. {mcp_agent → fast_agent}/agents/workflow/chain_agent.py +43 -33
  9. {mcp_agent → fast_agent}/agents/workflow/evaluator_optimizer.py +31 -35
  10. {mcp_agent → fast_agent}/agents/workflow/iterative_planner.py +56 -47
  11. {mcp_agent → fast_agent}/agents/workflow/orchestrator_models.py +4 -4
  12. {mcp_agent → fast_agent}/agents/workflow/parallel_agent.py +34 -41
  13. {mcp_agent → fast_agent}/agents/workflow/router_agent.py +54 -39
  14. {mcp_agent → fast_agent}/cli/__main__.py +5 -3
  15. {mcp_agent → fast_agent}/cli/commands/check_config.py +95 -66
  16. {mcp_agent → fast_agent}/cli/commands/go.py +20 -11
  17. {mcp_agent → fast_agent}/cli/commands/quickstart.py +4 -4
  18. {mcp_agent → fast_agent}/cli/commands/server_helpers.py +1 -1
  19. {mcp_agent → fast_agent}/cli/commands/setup.py +64 -134
  20. {mcp_agent → fast_agent}/cli/commands/url_parser.py +9 -8
  21. {mcp_agent → fast_agent}/cli/main.py +36 -16
  22. {mcp_agent → fast_agent}/cli/terminal.py +2 -2
  23. {mcp_agent → fast_agent}/config.py +10 -2
  24. fast_agent/constants.py +8 -0
  25. {mcp_agent → fast_agent}/context.py +24 -19
  26. {mcp_agent → fast_agent}/context_dependent.py +9 -5
  27. fast_agent/core/__init__.py +17 -0
  28. {mcp_agent → fast_agent}/core/agent_app.py +39 -36
  29. fast_agent/core/core_app.py +135 -0
  30. {mcp_agent → fast_agent}/core/direct_decorators.py +12 -26
  31. {mcp_agent → fast_agent}/core/direct_factory.py +95 -73
  32. {mcp_agent → fast_agent/core}/executor/executor.py +4 -5
  33. {mcp_agent → fast_agent}/core/fastagent.py +32 -32
  34. fast_agent/core/logging/__init__.py +5 -0
  35. {mcp_agent → fast_agent/core}/logging/events.py +3 -3
  36. {mcp_agent → fast_agent/core}/logging/json_serializer.py +1 -1
  37. {mcp_agent → fast_agent/core}/logging/listeners.py +85 -7
  38. {mcp_agent → fast_agent/core}/logging/logger.py +7 -7
  39. {mcp_agent → fast_agent/core}/logging/transport.py +10 -11
  40. fast_agent/core/prompt.py +9 -0
  41. {mcp_agent → fast_agent}/core/validation.py +4 -4
  42. fast_agent/event_progress.py +61 -0
  43. fast_agent/history/history_exporter.py +44 -0
  44. {mcp_agent → fast_agent}/human_input/__init__.py +9 -12
  45. {mcp_agent → fast_agent}/human_input/elicitation_handler.py +26 -8
  46. {mcp_agent → fast_agent}/human_input/elicitation_state.py +7 -7
  47. {mcp_agent → fast_agent}/human_input/simple_form.py +6 -4
  48. {mcp_agent → fast_agent}/human_input/types.py +1 -18
  49. fast_agent/interfaces.py +228 -0
  50. fast_agent/llm/__init__.py +9 -0
  51. mcp_agent/llm/augmented_llm.py → fast_agent/llm/fastagent_llm.py +127 -218
  52. fast_agent/llm/internal/passthrough.py +137 -0
  53. mcp_agent/llm/augmented_llm_playback.py → fast_agent/llm/internal/playback.py +29 -25
  54. mcp_agent/llm/augmented_llm_silent.py → fast_agent/llm/internal/silent.py +10 -17
  55. fast_agent/llm/internal/slow.py +38 -0
  56. {mcp_agent → fast_agent}/llm/memory.py +40 -30
  57. {mcp_agent → fast_agent}/llm/model_database.py +35 -2
  58. {mcp_agent → fast_agent}/llm/model_factory.py +103 -77
  59. fast_agent/llm/model_info.py +126 -0
  60. {mcp_agent/llm/providers → fast_agent/llm/provider/anthropic}/anthropic_utils.py +7 -7
  61. fast_agent/llm/provider/anthropic/llm_anthropic.py +603 -0
  62. {mcp_agent/llm/providers → fast_agent/llm/provider/anthropic}/multipart_converter_anthropic.py +79 -86
  63. {mcp_agent/llm/providers → fast_agent/llm/provider/bedrock}/bedrock_utils.py +3 -1
  64. mcp_agent/llm/providers/augmented_llm_bedrock.py → fast_agent/llm/provider/bedrock/llm_bedrock.py +833 -717
  65. {mcp_agent/llm/providers → fast_agent/llm/provider/google}/google_converter.py +66 -14
  66. fast_agent/llm/provider/google/llm_google_native.py +431 -0
  67. mcp_agent/llm/providers/augmented_llm_aliyun.py → fast_agent/llm/provider/openai/llm_aliyun.py +6 -7
  68. mcp_agent/llm/providers/augmented_llm_azure.py → fast_agent/llm/provider/openai/llm_azure.py +4 -4
  69. mcp_agent/llm/providers/augmented_llm_deepseek.py → fast_agent/llm/provider/openai/llm_deepseek.py +10 -11
  70. mcp_agent/llm/providers/augmented_llm_generic.py → fast_agent/llm/provider/openai/llm_generic.py +4 -4
  71. mcp_agent/llm/providers/augmented_llm_google_oai.py → fast_agent/llm/provider/openai/llm_google_oai.py +4 -4
  72. mcp_agent/llm/providers/augmented_llm_groq.py → fast_agent/llm/provider/openai/llm_groq.py +14 -16
  73. mcp_agent/llm/providers/augmented_llm_openai.py → fast_agent/llm/provider/openai/llm_openai.py +133 -207
  74. mcp_agent/llm/providers/augmented_llm_openrouter.py → fast_agent/llm/provider/openai/llm_openrouter.py +6 -6
  75. mcp_agent/llm/providers/augmented_llm_tensorzero_openai.py → fast_agent/llm/provider/openai/llm_tensorzero_openai.py +17 -16
  76. mcp_agent/llm/providers/augmented_llm_xai.py → fast_agent/llm/provider/openai/llm_xai.py +6 -6
  77. {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/multipart_converter_openai.py +125 -63
  78. {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/openai_multipart.py +12 -12
  79. {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/openai_utils.py +18 -16
  80. {mcp_agent → fast_agent}/llm/provider_key_manager.py +2 -2
  81. {mcp_agent → fast_agent}/llm/provider_types.py +2 -0
  82. {mcp_agent → fast_agent}/llm/sampling_converter.py +15 -12
  83. {mcp_agent → fast_agent}/llm/usage_tracking.py +23 -5
  84. fast_agent/mcp/__init__.py +43 -0
  85. {mcp_agent → fast_agent}/mcp/elicitation_factory.py +3 -3
  86. {mcp_agent → fast_agent}/mcp/elicitation_handlers.py +19 -10
  87. {mcp_agent → fast_agent}/mcp/gen_client.py +3 -3
  88. fast_agent/mcp/helpers/__init__.py +36 -0
  89. fast_agent/mcp/helpers/content_helpers.py +183 -0
  90. {mcp_agent → fast_agent}/mcp/helpers/server_config_helpers.py +8 -8
  91. {mcp_agent → fast_agent}/mcp/hf_auth.py +25 -23
  92. fast_agent/mcp/interfaces.py +93 -0
  93. {mcp_agent → fast_agent}/mcp/logger_textio.py +4 -4
  94. {mcp_agent → fast_agent}/mcp/mcp_agent_client_session.py +49 -44
  95. {mcp_agent → fast_agent}/mcp/mcp_aggregator.py +66 -115
  96. {mcp_agent → fast_agent}/mcp/mcp_connection_manager.py +16 -23
  97. {mcp_agent/core → fast_agent/mcp}/mcp_content.py +23 -15
  98. {mcp_agent → fast_agent}/mcp/mime_utils.py +39 -0
  99. fast_agent/mcp/prompt.py +159 -0
  100. mcp_agent/mcp/prompt_message_multipart.py → fast_agent/mcp/prompt_message_extended.py +27 -20
  101. {mcp_agent → fast_agent}/mcp/prompt_render.py +21 -19
  102. {mcp_agent → fast_agent}/mcp/prompt_serialization.py +46 -46
  103. fast_agent/mcp/prompts/__main__.py +7 -0
  104. {mcp_agent → fast_agent}/mcp/prompts/prompt_helpers.py +31 -30
  105. {mcp_agent → fast_agent}/mcp/prompts/prompt_load.py +8 -8
  106. {mcp_agent → fast_agent}/mcp/prompts/prompt_server.py +11 -19
  107. {mcp_agent → fast_agent}/mcp/prompts/prompt_template.py +18 -18
  108. {mcp_agent → fast_agent}/mcp/resource_utils.py +1 -1
  109. {mcp_agent → fast_agent}/mcp/sampling.py +31 -26
  110. {mcp_agent/mcp_server → fast_agent/mcp/server}/__init__.py +1 -1
  111. {mcp_agent/mcp_server → fast_agent/mcp/server}/agent_server.py +5 -6
  112. fast_agent/mcp/ui_agent.py +48 -0
  113. fast_agent/mcp/ui_mixin.py +209 -0
  114. fast_agent/mcp_server_registry.py +90 -0
  115. {mcp_agent → fast_agent}/resources/examples/data-analysis/analysis-campaign.py +5 -4
  116. {mcp_agent → fast_agent}/resources/examples/data-analysis/analysis.py +1 -1
  117. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/forms_demo.py +3 -3
  118. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character.py +2 -2
  119. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character_handler.py +1 -1
  120. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/tool_call.py +1 -1
  121. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_one.py +1 -1
  122. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_two.py +1 -1
  123. {mcp_agent → fast_agent}/resources/examples/researcher/researcher-eval.py +1 -1
  124. {mcp_agent → fast_agent}/resources/examples/researcher/researcher-imp.py +1 -1
  125. {mcp_agent → fast_agent}/resources/examples/researcher/researcher.py +1 -1
  126. {mcp_agent → fast_agent}/resources/examples/tensorzero/agent.py +2 -2
  127. {mcp_agent → fast_agent}/resources/examples/tensorzero/image_demo.py +3 -3
  128. {mcp_agent → fast_agent}/resources/examples/tensorzero/simple_agent.py +1 -1
  129. {mcp_agent → fast_agent}/resources/examples/workflows/chaining.py +1 -1
  130. {mcp_agent → fast_agent}/resources/examples/workflows/evaluator.py +3 -3
  131. {mcp_agent → fast_agent}/resources/examples/workflows/human_input.py +5 -3
  132. {mcp_agent → fast_agent}/resources/examples/workflows/orchestrator.py +1 -1
  133. {mcp_agent → fast_agent}/resources/examples/workflows/parallel.py +2 -2
  134. {mcp_agent → fast_agent}/resources/examples/workflows/router.py +5 -2
  135. fast_agent/resources/setup/.gitignore +24 -0
  136. fast_agent/resources/setup/agent.py +18 -0
  137. fast_agent/resources/setup/fastagent.config.yaml +44 -0
  138. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  139. fast_agent/tools/elicitation.py +369 -0
  140. fast_agent/types/__init__.py +32 -0
  141. fast_agent/types/llm_stop_reason.py +77 -0
  142. fast_agent/ui/__init__.py +38 -0
  143. fast_agent/ui/console_display.py +1005 -0
  144. {mcp_agent/human_input → fast_agent/ui}/elicitation_form.py +17 -12
  145. mcp_agent/human_input/elicitation_forms.py → fast_agent/ui/elicitation_style.py +1 -1
  146. {mcp_agent/core → fast_agent/ui}/enhanced_prompt.py +96 -25
  147. {mcp_agent/core → fast_agent/ui}/interactive_prompt.py +330 -125
  148. fast_agent/ui/mcp_ui_utils.py +224 -0
  149. {mcp_agent → fast_agent/ui}/progress_display.py +2 -2
  150. {mcp_agent/logging → fast_agent/ui}/rich_progress.py +4 -4
  151. {mcp_agent/core → fast_agent/ui}/usage_display.py +3 -8
  152. {fast_agent_mcp-0.2.58.dist-info → fast_agent_mcp-0.3.0.dist-info}/METADATA +7 -7
  153. fast_agent_mcp-0.3.0.dist-info/RECORD +202 -0
  154. fast_agent_mcp-0.3.0.dist-info/entry_points.txt +5 -0
  155. fast_agent_mcp-0.2.58.dist-info/RECORD +0 -193
  156. fast_agent_mcp-0.2.58.dist-info/entry_points.txt +0 -6
  157. mcp_agent/__init__.py +0 -114
  158. mcp_agent/agents/agent.py +0 -92
  159. mcp_agent/agents/workflow/__init__.py +0 -1
  160. mcp_agent/agents/workflow/orchestrator_agent.py +0 -597
  161. mcp_agent/app.py +0 -175
  162. mcp_agent/core/__init__.py +0 -26
  163. mcp_agent/core/prompt.py +0 -191
  164. mcp_agent/event_progress.py +0 -134
  165. mcp_agent/human_input/handler.py +0 -81
  166. mcp_agent/llm/__init__.py +0 -2
  167. mcp_agent/llm/augmented_llm_passthrough.py +0 -232
  168. mcp_agent/llm/augmented_llm_slow.py +0 -53
  169. mcp_agent/llm/providers/__init__.py +0 -8
  170. mcp_agent/llm/providers/augmented_llm_anthropic.py +0 -718
  171. mcp_agent/llm/providers/augmented_llm_google_native.py +0 -496
  172. mcp_agent/llm/providers/sampling_converter_anthropic.py +0 -57
  173. mcp_agent/llm/providers/sampling_converter_openai.py +0 -26
  174. mcp_agent/llm/sampling_format_converter.py +0 -37
  175. mcp_agent/logging/__init__.py +0 -0
  176. mcp_agent/mcp/__init__.py +0 -50
  177. mcp_agent/mcp/helpers/__init__.py +0 -25
  178. mcp_agent/mcp/helpers/content_helpers.py +0 -187
  179. mcp_agent/mcp/interfaces.py +0 -266
  180. mcp_agent/mcp/prompts/__init__.py +0 -0
  181. mcp_agent/mcp/prompts/__main__.py +0 -10
  182. mcp_agent/mcp_server_registry.py +0 -343
  183. mcp_agent/tools/tool_definition.py +0 -14
  184. mcp_agent/ui/console_display.py +0 -790
  185. mcp_agent/ui/console_display_legacy.py +0 -401
  186. {mcp_agent → fast_agent}/agents/workflow/orchestrator_prompts.py +0 -0
  187. {mcp_agent/agents → fast_agent/cli}/__init__.py +0 -0
  188. {mcp_agent → fast_agent}/cli/constants.py +0 -0
  189. {mcp_agent → fast_agent}/core/error_handling.py +0 -0
  190. {mcp_agent → fast_agent}/core/exceptions.py +0 -0
  191. {mcp_agent/cli → fast_agent/core/executor}/__init__.py +0 -0
  192. {mcp_agent → fast_agent/core}/executor/task_registry.py +0 -0
  193. {mcp_agent → fast_agent/core}/executor/workflow_signal.py +0 -0
  194. {mcp_agent → fast_agent}/human_input/form_fields.py +0 -0
  195. {mcp_agent → fast_agent}/llm/prompt_utils.py +0 -0
  196. {mcp_agent/core → fast_agent/llm}/request_params.py +0 -0
  197. {mcp_agent → fast_agent}/mcp/common.py +0 -0
  198. {mcp_agent/executor → fast_agent/mcp/prompts}/__init__.py +0 -0
  199. {mcp_agent → fast_agent}/mcp/prompts/prompt_constants.py +0 -0
  200. {mcp_agent → fast_agent}/py.typed +0 -0
  201. {mcp_agent → fast_agent}/resources/examples/data-analysis/fastagent.config.yaml +0 -0
  202. {mcp_agent → fast_agent}/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -0
  203. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_account_server.py +0 -0
  204. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_forms_server.py +0 -0
  205. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_game_server.py +0 -0
  206. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.config.yaml +0 -0
  207. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +0 -0
  208. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.config.yaml +0 -0
  209. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +0 -0
  210. {mcp_agent → fast_agent}/resources/examples/researcher/fastagent.config.yaml +0 -0
  211. {mcp_agent → fast_agent}/resources/examples/tensorzero/.env.sample +0 -0
  212. {mcp_agent → fast_agent}/resources/examples/tensorzero/Makefile +0 -0
  213. {mcp_agent → fast_agent}/resources/examples/tensorzero/README.md +0 -0
  214. {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  215. {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/crab.png +0 -0
  216. {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  217. {mcp_agent → fast_agent}/resources/examples/tensorzero/docker-compose.yml +0 -0
  218. {mcp_agent → fast_agent}/resources/examples/tensorzero/fastagent.config.yaml +0 -0
  219. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/Dockerfile +0 -0
  220. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/entrypoint.sh +0 -0
  221. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/mcp_server.py +0 -0
  222. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/pyproject.toml +0 -0
  223. {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_schema.json +0 -0
  224. {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +0 -0
  225. {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +0 -0
  226. {mcp_agent → fast_agent}/resources/examples/workflows/fastagent.config.yaml +0 -0
  227. {mcp_agent → fast_agent}/resources/examples/workflows/graded_report.md +0 -0
  228. {mcp_agent → fast_agent}/resources/examples/workflows/short_story.md +0 -0
  229. {mcp_agent → fast_agent}/resources/examples/workflows/short_story.txt +0 -0
  230. {mcp_agent → fast_agent/ui}/console.py +0 -0
  231. {mcp_agent/core → fast_agent/ui}/mermaid_utils.py +0 -0
  232. {fast_agent_mcp-0.2.58.dist-info → fast_agent_mcp-0.3.0.dist-info}/WHEEL +0 -0
  233. {fast_agent_mcp-0.2.58.dist-info → fast_agent_mcp-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,22 +7,23 @@ and delegates operations to an attached AugmentedLLMProtocol instance.
7
7
 
8
8
  import asyncio
9
9
  import fnmatch
10
- import uuid
10
+ from abc import ABC
11
11
  from typing import (
12
12
  TYPE_CHECKING,
13
13
  Any,
14
- Callable,
15
14
  Dict,
16
15
  List,
17
16
  Mapping,
18
17
  Optional,
18
+ Sequence,
19
19
  Tuple,
20
20
  Type,
21
21
  TypeVar,
22
22
  Union,
23
23
  )
24
24
 
25
- from a2a.types import AgentCapabilities, AgentCard, AgentSkill
25
+ import mcp
26
+ from a2a.types import AgentCard, AgentSkill
26
27
  from mcp.types import (
27
28
  CallToolResult,
28
29
  EmbeddedResource,
@@ -33,42 +34,38 @@ from mcp.types import (
33
34
  TextContent,
34
35
  Tool,
35
36
  )
36
- from opentelemetry import trace
37
37
  from pydantic import BaseModel
38
38
 
39
- from mcp_agent.core.agent_types import AgentConfig, AgentType
40
- from mcp_agent.core.exceptions import PromptExitError
41
- from mcp_agent.core.prompt import Prompt
42
- from mcp_agent.core.request_params import RequestParams
43
- from mcp_agent.human_input.types import (
44
- HUMAN_INPUT_SIGNAL_NAME,
45
- HumanInputCallback,
46
- HumanInputRequest,
47
- HumanInputResponse,
39
+ from fast_agent.agents.agent_types import AgentConfig, AgentType
40
+ from fast_agent.agents.llm_agent import DEFAULT_CAPABILITIES
41
+ from fast_agent.agents.tool_agent import ToolAgent
42
+ from fast_agent.constants import HUMAN_INPUT_TOOL_NAME
43
+ from fast_agent.core.exceptions import PromptExitError
44
+ from fast_agent.core.logging.logger import get_logger
45
+ from fast_agent.interfaces import FastAgentLLMProtocol
46
+ from fast_agent.mcp.helpers.content_helpers import normalize_to_extended_list
47
+ from fast_agent.mcp.mcp_aggregator import MCPAggregator
48
+ from fast_agent.tools.elicitation import (
49
+ get_elicitation_tool,
50
+ run_elicitation_form,
51
+ set_elicitation_input_callback,
48
52
  )
49
- from mcp_agent.logging.logger import get_logger
50
- from mcp_agent.mcp.interfaces import AgentProtocol, AugmentedLLMProtocol
51
- from mcp_agent.mcp.mcp_aggregator import MCPAggregator
52
- from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
53
+ from fast_agent.types import PromptMessageExtended, RequestParams
53
54
 
54
55
  # Define a TypeVar for models
55
56
  ModelT = TypeVar("ModelT", bound=BaseModel)
56
57
 
57
58
  # Define a TypeVar for AugmentedLLM and its subclasses
58
- LLM = TypeVar("LLM", bound=AugmentedLLMProtocol)
59
+ LLM = TypeVar("LLM", bound=FastAgentLLMProtocol)
59
60
 
60
- HUMAN_INPUT_TOOL_NAME = "__human_input__"
61
61
  if TYPE_CHECKING:
62
- from mcp_agent.context import Context
63
- from mcp_agent.llm.usage_tracking import UsageAccumulator
62
+ from rich.text import Text
64
63
 
65
-
66
- DEFAULT_CAPABILITIES = AgentCapabilities(
67
- streaming=False, pushNotifications=False, stateTransitionHistory=False
68
- )
64
+ from fast_agent.context import Context
65
+ from fast_agent.llm.usage_tracking import UsageAccumulator
69
66
 
70
67
 
71
- class BaseAgent(MCPAggregator, AgentProtocol):
68
+ class McpAgent(ABC, ToolAgent):
72
69
  """
73
70
  A base Agent class that implements the AgentProtocol interface.
74
71
 
@@ -79,258 +76,141 @@ class BaseAgent(MCPAggregator, AgentProtocol):
79
76
  def __init__(
80
77
  self,
81
78
  config: AgentConfig,
82
- functions: Optional[List[Callable]] = None,
83
79
  connection_persistence: bool = True,
84
- human_input_callback: Optional[HumanInputCallback] = None,
85
- context: Optional["Context"] = None,
86
- **kwargs: Dict[str, Any],
80
+ # legacy human_input_callback removed
81
+ context: "Context | None" = None,
82
+ **kwargs,
87
83
  ) -> None:
88
- self.config = config
89
-
90
84
  super().__init__(
85
+ config=config,
91
86
  context=context,
87
+ **kwargs,
88
+ )
89
+
90
+ # Create aggregator with composition
91
+ self._aggregator = MCPAggregator(
92
92
  server_names=self.config.servers,
93
93
  connection_persistence=connection_persistence,
94
94
  name=self.config.name,
95
+ context=context,
96
+ config=self.config, # Pass the full config for access to elicitation_handler
95
97
  **kwargs,
96
98
  )
97
99
 
98
- self._context = context
99
- self.tracer = trace.get_tracer(__name__)
100
- self.name = self.config.name
101
100
  self.instruction = self.config.instruction
102
- self.functions = functions or []
103
- self.executor = self.context.executor if context and hasattr(context, "executor") else None
104
- self.logger = get_logger(f"{__name__}.{self.name}")
101
+ self.executor = context.executor if context else None
102
+ self.logger = get_logger(f"{__name__}.{self._name}")
105
103
 
106
104
  # Store the default request params from config
107
105
  self._default_request_params = self.config.default_request_params
108
106
 
109
- # Initialize the LLM to None (will be set by attach_llm)
110
- self._llm: Optional[AugmentedLLMProtocol] = None
107
+ # set with the "attach" method
108
+ self._llm: FastAgentLLMProtocol | None = None
111
109
 
112
- # Map function names to tools
113
- self._function_tool_map: Dict[str, Any] = {}
110
+ # Instantiate human input tool once if enabled in config
111
+ self._human_input_tool: Tool | None = None
112
+ if self.config.human_input:
113
+ try:
114
+ self._human_input_tool = get_elicitation_tool()
115
+ except Exception:
116
+ self._human_input_tool = None
114
117
 
115
- if not self.config.human_input:
116
- self.human_input_callback = None
117
- else:
118
- self.human_input_callback: Optional[HumanInputCallback] = human_input_callback
119
- if not human_input_callback and context and hasattr(context, "human_input_handler"):
120
- self.human_input_callback = context.human_input_handler
118
+ # Register the MCP UI handler as the elicitation callback so fast_agent.tools can call it
119
+ # without importing MCP types. This avoids circular imports and ensures the callback is ready.
120
+ try:
121
+ from fast_agent.human_input.elicitation_handler import elicitation_input_callback
122
+ from fast_agent.human_input.types import HumanInputRequest
123
+
124
+ async def _mcp_elicitation_adapter(
125
+ request_payload: dict,
126
+ agent_name: str | None = None,
127
+ server_name: str | None = None,
128
+ server_info: dict | None = None,
129
+ ) -> str:
130
+ req = HumanInputRequest(**request_payload)
131
+ resp = await elicitation_input_callback(
132
+ request=req,
133
+ agent_name=agent_name,
134
+ server_name=server_name,
135
+ server_info=server_info,
136
+ )
137
+ return resp.response if isinstance(resp.response, str) else str(resp.response)
138
+
139
+ set_elicitation_input_callback(_mcp_elicitation_adapter)
140
+ except Exception:
141
+ # If UI handler import fails, leave callback unset; tool will error with a clear message
142
+ pass
143
+
144
+ async def __aenter__(self):
145
+ """Initialize the agent and its MCP aggregator."""
146
+ await self._aggregator.__aenter__()
147
+ return self
148
+
149
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
150
+ """Clean up the agent and its MCP aggregator."""
151
+ await self._aggregator.__aexit__(exc_type, exc_val, exc_tb)
121
152
 
122
153
  async def initialize(self) -> None:
123
154
  """
124
155
  Initialize the agent and connect to the MCP servers.
125
156
  NOTE: This method is called automatically when the agent is used as an async context manager.
126
157
  """
127
- await self.__aenter__() # This initializes the connection manager and loads the servers
128
-
129
- async def attach_llm(
130
- self,
131
- llm_factory: Union[Type[AugmentedLLMProtocol], Callable[..., AugmentedLLMProtocol]],
132
- model: Optional[str] = None,
133
- request_params: Optional[RequestParams] = None,
134
- **additional_kwargs,
135
- ) -> AugmentedLLMProtocol:
136
- """
137
- Create and attach an LLM instance to this agent.
138
-
139
- Parameters have the following precedence (highest to lowest):
140
- 1. Explicitly passed parameters to this method
141
- 2. Agent's default_request_params
142
- 3. LLM's default values
143
-
144
- Args:
145
- llm_factory: A class or callable that constructs an AugmentedLLM
146
- model: Optional model name override
147
- request_params: Optional request parameters override
148
- **additional_kwargs: Additional parameters passed to the LLM constructor
149
-
150
- Returns:
151
- The created LLM instance
152
- """
153
- # Start with agent's default params
154
- effective_params = (
155
- self._default_request_params.model_copy() if self._default_request_params else None
156
- )
157
-
158
- # Override with explicitly passed request_params
159
- if request_params:
160
- if effective_params:
161
- # Update non-None values
162
- for k, v in request_params.model_dump(exclude_unset=True).items():
163
- if v is not None:
164
- setattr(effective_params, k, v)
165
- else:
166
- effective_params = request_params
167
-
168
- # Override model if explicitly specified
169
- if model and effective_params:
170
- effective_params.model = model
171
-
172
- # Create the LLM instance
173
- self._llm = llm_factory(
174
- agent=self, request_params=effective_params, context=self._context, **additional_kwargs
175
- )
176
-
177
- return self._llm
158
+ await self.__aenter__()
178
159
 
179
160
  async def shutdown(self) -> None:
180
161
  """
181
162
  Shutdown the agent and close all MCP server connections.
182
163
  NOTE: This method is called automatically when the agent is used as an async context manager.
183
164
  """
184
- await super().close()
165
+ await self._aggregator.close()
166
+
167
+ @property
168
+ def initialized(self) -> bool:
169
+ """Check if both the agent and aggregator are initialized."""
170
+ return self._initialized and self._aggregator.initialized
171
+
172
+ @initialized.setter
173
+ def initialized(self, value: bool) -> None:
174
+ """Set the initialized state of both agent and aggregator."""
175
+ self._initialized = value
176
+ self._aggregator.initialized = value
185
177
 
186
178
  async def __call__(
187
179
  self,
188
- message: Union[str, PromptMessageMultipart] | None = None,
189
- agent_name: Optional[str] = None,
190
- default_prompt: str = "",
180
+ message: Union[
181
+ str,
182
+ PromptMessage,
183
+ PromptMessageExtended,
184
+ Sequence[Union[str, PromptMessage, PromptMessageExtended]],
185
+ ],
191
186
  ) -> str:
192
- """
193
- Make the agent callable to send messages or start an interactive prompt.
194
-
195
- Args:
196
- message: Optional message to send to the agent
197
- agent_name: Optional name of the agent (for consistency with DirectAgentApp)
198
- default: Default message to use in interactive prompt mode
199
-
200
- Returns:
201
- The agent's response as a string or the result of the interactive session
202
- """
203
- if message:
204
- return await self.send(message)
205
- return await self.prompt(default_prompt=default_prompt)
206
-
207
- async def generate_str(self, message: str, request_params: RequestParams | None) -> str:
208
- result: PromptMessageMultipart = await self.generate([Prompt.user(message)], request_params)
209
- return result.first_text()
187
+ return await self.send(message)
210
188
 
211
189
  async def send(
212
- self,
213
- message: Union[str, PromptMessage, PromptMessageMultipart],
214
- request_params: RequestParams | None = None
190
+ self,
191
+ message: Union[
192
+ str,
193
+ PromptMessage,
194
+ PromptMessageExtended,
195
+ Sequence[Union[str, PromptMessage, PromptMessageExtended]],
196
+ ],
197
+ request_params: RequestParams | None = None,
215
198
  ) -> str:
216
199
  """
217
200
  Send a message to the agent and get a response.
218
201
 
219
202
  Args:
220
203
  message: Message content in various formats:
221
- - String: Converted to a user PromptMessageMultipart
222
- - PromptMessage: Converted to PromptMessageMultipart
223
- - PromptMessageMultipart: Used directly
204
+ - String: Converted to a user PromptMessageExtended
205
+ - PromptMessage: Converted to PromptMessageExtended
206
+ - PromptMessageExtended: Used directly
224
207
  - request_params: Optional request parameters
225
208
 
226
209
  Returns:
227
210
  The agent's response as a string
228
211
  """
229
- # Convert the input to a PromptMessageMultipart
230
- prompt = self._normalize_message_input(message)
231
-
232
- # Use the LLM to generate a response
233
- response = await self.generate([prompt], request_params)
234
- return response.all_text()
235
-
236
- def _normalize_message_input(
237
- self, message: Union[str, PromptMessage, PromptMessageMultipart]
238
- ) -> PromptMessageMultipart:
239
- """
240
- Convert a message of any supported type to PromptMessageMultipart.
241
-
242
- Args:
243
- message: Message in various formats (string, PromptMessage, or PromptMessageMultipart)
244
-
245
- Returns:
246
- A PromptMessageMultipart object
247
- """
248
- # Handle single message
249
- if isinstance(message, str):
250
- return Prompt.user(message)
251
- elif isinstance(message, PromptMessage):
252
- return PromptMessageMultipart(role=message.role, content=[message.content])
253
- elif isinstance(message, PromptMessageMultipart):
254
- return message
255
- else:
256
- # Try to convert to string as fallback
257
- return Prompt.user(str(message))
258
-
259
- async def prompt(self, default_prompt: str = "") -> str:
260
- """
261
- Start an interactive prompt session with the agent.
262
-
263
- Args:
264
- default_prompt: The initial prompt to send to the agent
265
-
266
- Returns:
267
- The result of the interactive session
268
- """
269
- ...
270
-
271
- async def request_human_input(self, request: HumanInputRequest) -> str:
272
- """
273
- Request input from a human user. Pauses the workflow until input is received.
274
-
275
- Args:
276
- request: The human input request
277
-
278
- Returns:
279
- The input provided by the human
280
-
281
- Raises:
282
- TimeoutError: If the timeout is exceeded
283
- """
284
- if not self.human_input_callback:
285
- raise ValueError("Human input callback not set")
286
-
287
- # Generate a unique ID for this request to avoid signal collisions
288
- request_id = f"{HUMAN_INPUT_SIGNAL_NAME}_{self.name}_{uuid.uuid4()}"
289
- request.request_id = request_id
290
- # Use metadata as a dictionary to pass agent name
291
- request.metadata = {"agent_name": self.name}
292
- self.logger.debug("Requesting human input:", data=request)
293
-
294
- if not self.executor:
295
- raise ValueError("No executor available")
296
-
297
- async def call_callback_and_signal() -> None:
298
- try:
299
- assert self.human_input_callback is not None
300
- user_input = await self.human_input_callback(request)
301
-
302
- self.logger.debug("Received human input:", data=user_input)
303
- await self.executor.signal(signal_name=request_id, payload=user_input)
304
- except PromptExitError as e:
305
- # Propagate the exit error through the signal system
306
- self.logger.info("User requested to exit session")
307
- await self.executor.signal(
308
- signal_name=request_id,
309
- payload={"exit_requested": True, "error": str(e)},
310
- )
311
- except Exception as e:
312
- await self.executor.signal(
313
- request_id, payload=f"Error getting human input: {str(e)}"
314
- )
315
-
316
- asyncio.create_task(call_callback_and_signal())
317
-
318
- self.logger.debug("Waiting for human input signal")
319
-
320
- # Wait for signal (workflow is paused here)
321
- result = await self.executor.wait_for_signal(
322
- signal_name=request_id,
323
- request_id=request_id,
324
- workflow_id=request.workflow_id,
325
- signal_description=request.description or request.prompt,
326
- timeout_seconds=request.timeout_seconds,
327
- signal_type=HumanInputResponse,
328
- )
329
-
330
- if isinstance(result, dict) and result.get("exit_requested", False):
331
- raise PromptExitError(result.get("error", "User requested to exit FastAgent session"))
332
- self.logger.debug("Received human input signal", data=result)
333
- return result
212
+ response = await self.generate(message, request_params)
213
+ return response.last_text() or ""
334
214
 
335
215
  def _matches_pattern(self, name: str, pattern: str, server_name: str) -> bool:
336
216
  """
@@ -359,11 +239,8 @@ class BaseAgent(MCPAggregator, AgentProtocol):
359
239
  Returns:
360
240
  ListToolsResult with available tools
361
241
  """
362
- if not self.initialized:
363
- await self.initialize()
364
-
365
- # Get all tools from the parent class
366
- result = await super().list_tools()
242
+ # Get all tools from the aggregator
243
+ result = await self._aggregator.list_tools()
367
244
 
368
245
  # Apply filtering if tools are specified in config
369
246
  if self.config.tools is not None:
@@ -378,27 +255,16 @@ class BaseAgent(MCPAggregator, AgentProtocol):
378
255
 
379
256
  # Check if this server has tool filters
380
257
  if server_name and server_name in self.config.tools:
381
- # Check if tool matches any pattern for this server
382
- for pattern in self.config.tools[server_name]:
383
- if self._matches_pattern(tool.name, pattern, server_name):
384
- filtered_tools.append(tool)
385
- break
258
+ # Check if tool matches any pattern for this server
259
+ for pattern in self.config.tools[server_name]:
260
+ if self._matches_pattern(tool.name, pattern, server_name):
261
+ filtered_tools.append(tool)
262
+ break
386
263
  result.tools = filtered_tools
387
264
 
388
- if not self.human_input_callback:
389
- return result
390
-
391
- # Add a human_input_callback as a tool
392
- from mcp.server.fastmcp.tools import Tool as FastTool
393
-
394
- human_input_tool: FastTool = FastTool.from_function(self.request_human_input)
395
- result.tools.append(
396
- Tool(
397
- name=HUMAN_INPUT_TOOL_NAME,
398
- description=human_input_tool.description,
399
- inputSchema=human_input_tool.parameters,
400
- )
401
- )
265
+ # Append human input tool if enabled and available
266
+ if self.config.human_input and getattr(self, "_human_input_tool", None):
267
+ result.tools.append(self._human_input_tool)
402
268
 
403
269
  return result
404
270
 
@@ -414,50 +280,43 @@ class BaseAgent(MCPAggregator, AgentProtocol):
414
280
  Result of the tool call
415
281
  """
416
282
  if name == HUMAN_INPUT_TOOL_NAME:
417
- # Call the human input tool
283
+ # Call the elicitation-backed human input tool
418
284
  return await self._call_human_input_tool(arguments)
419
285
  else:
420
- return await super().call_tool(name, arguments)
286
+ return await self._aggregator.call_tool(name, arguments)
421
287
 
422
288
  async def _call_human_input_tool(
423
289
  self, arguments: Dict[str, Any] | None = None
424
290
  ) -> CallToolResult:
425
291
  """
426
- Handle human input request via tool calling.
292
+ Handle human input via an elicitation form.
427
293
 
428
- Args:
429
- arguments: Tool arguments
294
+ Expected inputs:
295
+ - Either an object with optional 'message' and a 'schema' JSON Schema (object), or
296
+ - The JSON Schema (object) itself as the arguments.
430
297
 
431
- Returns:
432
- Result of the human input request
298
+ Constraints:
299
+ - No more than 7 top-level properties are allowed in the schema.
433
300
  """
434
- # Handle human input request
435
301
  try:
436
- # Make sure arguments is not None
437
- if arguments is None:
438
- arguments = {}
439
-
440
- # Extract request data
441
- request_data = arguments.get("request")
442
-
443
- # Handle both string and dict request formats
444
- if isinstance(request_data, str):
445
- request = HumanInputRequest(prompt=request_data)
446
- elif isinstance(request_data, dict):
447
- request = HumanInputRequest(**request_data)
448
- else:
449
- # Fallback for invalid or missing request data
450
- request = HumanInputRequest(prompt="Please provide input:")
451
-
452
- result = await self.request_human_input(request=request)
453
-
454
- # Use response attribute if available, otherwise use the result directly
455
- response_text = (
456
- result.response if isinstance(result, HumanInputResponse) else str(result)
457
- )
458
-
302
+ # Run via shared tool runner
303
+ resp_text = await run_elicitation_form(arguments, agent_name=self._name)
304
+ if resp_text == "__DECLINED__":
305
+ return CallToolResult(
306
+ isError=False,
307
+ content=[TextContent(type="text", text="The Human declined the input request")],
308
+ )
309
+ if resp_text in ("__CANCELLED__", "__DISABLE_SERVER__"):
310
+ return CallToolResult(
311
+ isError=False,
312
+ content=[
313
+ TextContent(type="text", text="The Human cancelled the input request")
314
+ ],
315
+ )
316
+ # Success path: return the (JSON) response as-is
459
317
  return CallToolResult(
460
- content=[TextContent(type="text", text=f"Human response: {response_text}")]
318
+ isError=False,
319
+ content=[TextContent(type="text", text=resp_text)],
461
320
  )
462
321
 
463
322
  except PromptExitError:
@@ -476,7 +335,6 @@ class BaseAgent(MCPAggregator, AgentProtocol):
476
335
  import traceback
477
336
 
478
337
  print(f"Error in _call_human_input_tool: {traceback.format_exc()}")
479
-
480
338
  return CallToolResult(
481
339
  isError=True,
482
340
  content=[TextContent(type="text", text=f"Error requesting human input: {str(e)}")],
@@ -486,6 +344,7 @@ class BaseAgent(MCPAggregator, AgentProtocol):
486
344
  self,
487
345
  prompt_name: str,
488
346
  arguments: Dict[str, str] | None = None,
347
+ namespace: str | None = None,
489
348
  server_name: str | None = None,
490
349
  ) -> GetPromptResult:
491
350
  """
@@ -494,20 +353,21 @@ class BaseAgent(MCPAggregator, AgentProtocol):
494
353
  Args:
495
354
  prompt_name: Name of the prompt, optionally namespaced
496
355
  arguments: Optional dictionary of arguments to pass to the prompt template
497
- server_name: Optional name of the server to get the prompt from
356
+ namespace: Optional namespace (server) to get the prompt from
498
357
 
499
358
  Returns:
500
359
  GetPromptResult containing the prompt information
501
360
  """
502
- return await super().get_prompt(prompt_name, arguments, server_name)
361
+ target = namespace if namespace is not None else server_name
362
+ return await self._aggregator.get_prompt(prompt_name, arguments, target)
503
363
 
504
364
  async def apply_prompt(
505
365
  self,
506
366
  prompt: Union[str, GetPromptResult],
507
367
  arguments: Dict[str, str] | None = None,
508
- agent_name: str | None = None,
509
- server_name: str | None = None,
510
368
  as_template: bool = False,
369
+ namespace: str | None = None,
370
+ **_: Any,
511
371
  ) -> str:
512
372
  """
513
373
  Apply an MCP Server Prompt by name or GetPromptResult and return the assistant's response.
@@ -519,9 +379,8 @@ class BaseAgent(MCPAggregator, AgentProtocol):
519
379
  Args:
520
380
  prompt: The name of the prompt to apply OR a GetPromptResult object
521
381
  arguments: Optional dictionary of string arguments to pass to the prompt template
522
- agent_name: Optional agent name (ignored at this level, used by multi-agent apps)
523
- server_name: Optional name of the server to get the prompt from
524
382
  as_template: If True, store as persistent template (always included in context)
383
+ namespace: Optional namespace/server to resolve the prompt from
525
384
 
526
385
  Returns:
527
386
  The assistant's response or error message
@@ -533,7 +392,7 @@ class BaseAgent(MCPAggregator, AgentProtocol):
533
392
  # Get the prompt - this will search all servers if needed
534
393
  self.logger.debug(f"Loading prompt '{prompt_name}'")
535
394
  prompt_result: GetPromptResult = await self.get_prompt(
536
- prompt_name, arguments, server_name
395
+ prompt_name, arguments, namespace
537
396
  )
538
397
 
539
398
  if not prompt_result or not prompt_result.messages:
@@ -557,7 +416,7 @@ class BaseAgent(MCPAggregator, AgentProtocol):
557
416
  self.logger.debug(f"Using prompt '{namespaced_name}'")
558
417
 
559
418
  # Convert prompt messages to multipart format using the safer method
560
- multipart_messages = PromptMessageMultipart.from_get_prompt_result(prompt_result)
419
+ multipart_messages = PromptMessageExtended.from_get_prompt_result(prompt_result)
561
420
 
562
421
  if as_template:
563
422
  # Use apply_prompt_template to store as persistent prompt messages
@@ -579,13 +438,13 @@ class BaseAgent(MCPAggregator, AgentProtocol):
579
438
  server_name: Optional name of the MCP server to retrieve the resource from
580
439
 
581
440
  Returns:
582
- List of EmbeddedResource objects ready to use in a PromptMessageMultipart
441
+ List of EmbeddedResource objects ready to use in a PromptMessageExtended
583
442
 
584
443
  Raises:
585
444
  ValueError: If the server doesn't exist or the resource couldn't be found
586
445
  """
587
446
  # Get the raw resource result
588
- result: ReadResourceResult = await self.get_resource(resource_uri, server_name)
447
+ result: ReadResourceResult = await self._aggregator.get_resource(resource_uri, server_name)
589
448
 
590
449
  # Convert each resource content to an EmbeddedResource
591
450
  embedded_resources: List[EmbeddedResource] = []
@@ -597,10 +456,32 @@ class BaseAgent(MCPAggregator, AgentProtocol):
597
456
 
598
457
  return embedded_resources
599
458
 
459
+ async def get_resource(
460
+ self, resource_uri: str, namespace: str | None = None, server_name: str | None = None
461
+ ) -> ReadResourceResult:
462
+ """
463
+ Get a resource from an MCP server.
464
+
465
+ Args:
466
+ resource_uri: URI of the resource to retrieve
467
+ namespace: Optional namespace (server) to retrieve the resource from
468
+
469
+ Returns:
470
+ ReadResourceResult containing the resource data
471
+
472
+ Raises:
473
+ ValueError: If the server doesn't exist or the resource couldn't be found
474
+ """
475
+ # Get the raw resource result
476
+ target = namespace if namespace is not None else server_name
477
+ result: ReadResourceResult = await self._aggregator.get_resource(resource_uri, target)
478
+ return result
479
+
600
480
  async def with_resource(
601
481
  self,
602
- prompt_content: Union[str, PromptMessage, PromptMessageMultipart],
482
+ prompt_content: Union[str, PromptMessage, PromptMessageExtended],
603
483
  resource_uri: str,
484
+ namespace: str | None = None,
604
485
  server_name: str | None = None,
605
486
  ) -> str:
606
487
  """
@@ -609,62 +490,96 @@ class BaseAgent(MCPAggregator, AgentProtocol):
609
490
  Args:
610
491
  prompt_content: Content in various formats:
611
492
  - String: Converted to a user message with the text
612
- - PromptMessage: Converted to PromptMessageMultipart
613
- - PromptMessageMultipart: Used directly
493
+ - PromptMessage: Converted to PromptMessageExtended
494
+ - PromptMessageExtended: Used directly
614
495
  resource_uri: URI of the resource to retrieve
615
- server_name: Optional name of the MCP server to retrieve the resource from
496
+ namespace: Optional namespace (server) to retrieve the resource from
616
497
 
617
498
  Returns:
618
499
  The agent's response as a string
619
500
  """
620
501
  # Get the embedded resources
621
502
  embedded_resources: List[EmbeddedResource] = await self.get_embedded_resources(
622
- resource_uri, server_name
503
+ resource_uri, namespace if namespace is not None else server_name
623
504
  )
624
505
 
625
506
  # Create or update the prompt message
626
- prompt: PromptMessageMultipart
507
+ prompt: PromptMessageExtended
627
508
  if isinstance(prompt_content, str):
628
509
  # Create a new prompt with the text and resources
629
510
  content = [TextContent(type="text", text=prompt_content)]
630
511
  content.extend(embedded_resources)
631
- prompt = PromptMessageMultipart(role="user", content=content)
512
+ prompt = PromptMessageExtended(role="user", content=content)
632
513
  elif isinstance(prompt_content, PromptMessage):
633
- # Convert PromptMessage to PromptMessageMultipart and add resources
514
+ # Convert PromptMessage to PromptMessageExtended and add resources
634
515
  content = [prompt_content.content]
635
516
  content.extend(embedded_resources)
636
- prompt = PromptMessageMultipart(role=prompt_content.role, content=content)
637
- elif isinstance(prompt_content, PromptMessageMultipart):
517
+ prompt = PromptMessageExtended(role=prompt_content.role, content=content)
518
+ elif isinstance(prompt_content, PromptMessageExtended):
638
519
  # Add resources to the existing prompt
639
520
  prompt = prompt_content
640
521
  prompt.content.extend(embedded_resources)
641
522
  else:
642
523
  raise TypeError(
643
- "prompt_content must be a string, PromptMessage, or PromptMessageMultipart"
524
+ "prompt_content must be a string, PromptMessage, or PromptMessageExtended"
644
525
  )
645
526
 
646
- response: PromptMessageMultipart = await self.generate([prompt], None)
527
+ response: PromptMessageExtended = await self.generate([prompt], None)
647
528
  return response.first_text()
648
529
 
649
- async def generate(
650
- self,
651
- multipart_messages: List[PromptMessageMultipart],
652
- request_params: RequestParams | None = None,
653
- ) -> PromptMessageMultipart:
654
- """
655
- Create a completion with the LLM using the provided messages.
656
- Delegates to the attached LLM.
530
+ async def run_tools(self, request: PromptMessageExtended) -> PromptMessageExtended:
531
+ """Override ToolAgent's run_tools to use MCP tools via aggregator."""
532
+ if not request.tool_calls:
533
+ self.logger.warning("No tool calls found in request", data=request)
534
+ return PromptMessageExtended(role="user", tool_results={})
535
+
536
+ tool_results: dict[str, CallToolResult] = {}
537
+
538
+ # Cache available tool names (original, not namespaced) for display
539
+ available_tools = [
540
+ namespaced_tool.tool.name
541
+ for namespaced_tool in self._aggregator._namespaced_tool_map.values()
542
+ ]
543
+
544
+ # Process each tool call using our aggregator
545
+ for correlation_id, tool_request in request.tool_calls.items():
546
+ tool_name = tool_request.params.name
547
+ tool_args = tool_request.params.arguments or {}
548
+
549
+ # Get the original tool name for display (not namespaced)
550
+ namespaced_tool = self._aggregator._namespaced_tool_map.get(tool_name)
551
+ display_tool_name = namespaced_tool.tool.name if namespaced_tool else tool_name
552
+
553
+ self.display.show_tool_call(
554
+ name=self._name,
555
+ tool_args=tool_args,
556
+ bottom_items=available_tools,
557
+ tool_name=display_tool_name,
558
+ highlight_items=tool_name,
559
+ max_item_length=12,
560
+ )
657
561
 
658
- Args:
659
- multipart_messages: List of multipart messages to send to the LLM
660
- request_params: Optional parameters to configure the request
562
+ try:
563
+ # Use our aggregator to call the MCP tool
564
+ result = await self.call_tool(tool_name, tool_args)
565
+ tool_results[correlation_id] = result
661
566
 
662
- Returns:
663
- The LLM's response as a PromptMessageMultipart
664
- """
665
- assert self._llm
666
- with self.tracer.start_as_current_span(f"Agent: '{self.name}' generate"):
667
- return await self._llm.generate(multipart_messages, request_params)
567
+ # Show tool result (like ToolAgent does)
568
+ self.display.show_tool_result(name=self._name, result=result)
569
+
570
+ self.logger.debug(f"MCP tool {display_tool_name} executed successfully")
571
+ except Exception as e:
572
+ self.logger.error(f"MCP tool {display_tool_name} failed: {e}")
573
+ error_result = CallToolResult(
574
+ content=[TextContent(type="text", text=f"Error: {str(e)}")],
575
+ isError=True,
576
+ )
577
+ tool_results[correlation_id] = error_result
578
+
579
+ # Show error result too
580
+ self.display.show_tool_result(name=self._name, result=error_result)
581
+
582
+ return PromptMessageExtended(role="user", tool_results=tool_results)
668
583
 
669
584
  async def apply_prompt_template(self, prompt_result: GetPromptResult, prompt_name: str) -> str:
670
585
  """
@@ -679,21 +594,30 @@ class BaseAgent(MCPAggregator, AgentProtocol):
679
594
  String representation of the assistant's response if generated
680
595
  """
681
596
  assert self._llm
682
- with self.tracer.start_as_current_span(f"Agent: '{self.name}' apply_prompt_template"):
597
+ with self._tracer.start_as_current_span(f"Agent: '{self._name}' apply_prompt_template"):
683
598
  return await self._llm.apply_prompt_template(prompt_result, prompt_name)
684
599
 
685
600
  async def structured(
686
601
  self,
687
- multipart_messages: List[PromptMessageMultipart],
602
+ messages: Union[
603
+ str,
604
+ PromptMessage,
605
+ PromptMessageExtended,
606
+ List[Union[str, PromptMessage, PromptMessageExtended]],
607
+ ],
688
608
  model: Type[ModelT],
689
609
  request_params: RequestParams | None = None,
690
- ) -> Tuple[ModelT | None, PromptMessageMultipart]:
610
+ ) -> Tuple[ModelT | None, PromptMessageExtended]:
691
611
  """
692
612
  Apply the prompt and return the result as a Pydantic model.
693
- Delegates to the attached LLM.
613
+ Normalizes input messages and delegates to the attached LLM.
694
614
 
695
615
  Args:
696
- prompt: List of PromptMessageMultipart objects
616
+ messages: Message(s) in various formats:
617
+ - String: Converted to a user PromptMessageExtended
618
+ - PromptMessage: Converted to PromptMessageExtended
619
+ - PromptMessageExtended: Used directly
620
+ - List of any combination of the above
697
621
  model: The Pydantic model class to parse the result into
698
622
  request_params: Optional parameters to configure the LLM request
699
623
 
@@ -701,17 +625,20 @@ class BaseAgent(MCPAggregator, AgentProtocol):
701
625
  An instance of the specified model, or None if coercion fails
702
626
  """
703
627
  assert self._llm
704
- with self.tracer.start_as_current_span(f"Agent: '{self.name}' structured"):
705
- return await self._llm.structured(multipart_messages, model, request_params)
628
+ # Normalize all input types to a list of PromptMessageExtended
629
+ normalized_messages = normalize_to_extended_list(messages)
630
+
631
+ with self._tracer.start_as_current_span(f"Agent: '{self._name}' structured"):
632
+ return await self._llm.structured(normalized_messages, model, request_params)
706
633
 
707
634
  async def apply_prompt_messages(
708
- self, prompts: List[PromptMessageMultipart], request_params: RequestParams | None = None
635
+ self, prompts: List[PromptMessageExtended], request_params: RequestParams | None = None
709
636
  ) -> str:
710
637
  """
711
638
  Apply a list of prompt messages and return the result.
712
639
 
713
640
  Args:
714
- prompts: List of PromptMessageMultipart messages
641
+ prompts: List of PromptMessageExtended messages
715
642
  request_params: Optional request parameters
716
643
 
717
644
  Returns:
@@ -721,21 +648,21 @@ class BaseAgent(MCPAggregator, AgentProtocol):
721
648
  response = await self.generate(prompts, request_params)
722
649
  return response.first_text()
723
650
 
724
- async def list_prompts(self, server_name: str | None = None) -> Mapping[str, List[Prompt]]:
651
+ async def list_prompts(
652
+ self, namespace: str | None = None, server_name: str | None = None
653
+ ) -> Mapping[str, List[mcp.types.Prompt]]:
725
654
  """
726
655
  List all prompts available to this agent, filtered by configuration.
727
656
 
728
657
  Args:
729
- server_name: Optional server name to list prompts from
658
+ namespace: Optional namespace (server) to list prompts from
730
659
 
731
660
  Returns:
732
661
  Dictionary mapping server names to lists of Prompt objects
733
662
  """
734
- if not self.initialized:
735
- await self.initialize()
736
-
737
- # Get all prompts from the parent class
738
- result = await super().list_prompts(server_name)
663
+ # Get all prompts from the aggregator
664
+ target = namespace if namespace is not None else server_name
665
+ result = await self._aggregator.list_prompts(target)
739
666
 
740
667
  # Apply filtering if prompts are specified in config
741
668
  if self.config.prompts is not None:
@@ -756,21 +683,21 @@ class BaseAgent(MCPAggregator, AgentProtocol):
756
683
 
757
684
  return result
758
685
 
759
- async def list_resources(self, server_name: str | None = None) -> Dict[str, List[str]]:
686
+ async def list_resources(
687
+ self, namespace: str | None = None, server_name: str | None = None
688
+ ) -> Dict[str, List[str]]:
760
689
  """
761
690
  List all resources available to this agent, filtered by configuration.
762
691
 
763
692
  Args:
764
- server_name: Optional server name to list resources from
693
+ namespace: Optional namespace (server) to list resources from
765
694
 
766
695
  Returns:
767
696
  Dictionary mapping server names to lists of resource URIs
768
697
  """
769
- if not self.initialized:
770
- await self.initialize()
771
-
772
- # Get all resources from the parent class
773
- result = await super().list_resources(server_name)
698
+ # Get all resources from the aggregator
699
+ target = namespace if namespace is not None else server_name
700
+ result = await self._aggregator.list_resources(target)
774
701
 
775
702
  # Apply filtering if resources are specified in config
776
703
  if self.config.resources is not None:
@@ -791,21 +718,21 @@ class BaseAgent(MCPAggregator, AgentProtocol):
791
718
 
792
719
  return result
793
720
 
794
- async def list_mcp_tools(self, server_name: str | None = None) -> Mapping[str, List[Tool]]:
721
+ async def list_mcp_tools(
722
+ self, namespace: str | None = None, server_name: str | None = None
723
+ ) -> Mapping[str, List[Tool]]:
795
724
  """
796
725
  List all tools available to this agent, grouped by server and filtered by configuration.
797
726
 
798
727
  Args:
799
- server_name: Optional server name to list tools from
728
+ namespace: Optional namespace (server) to list tools from
800
729
 
801
730
  Returns:
802
731
  Dictionary mapping server names to lists of Tool objects (with original names, not namespaced)
803
732
  """
804
- if not self.initialized:
805
- await self.initialize()
806
-
807
- # Get all tools from the parent class
808
- result = await super().list_mcp_tools(server_name)
733
+ # Get all tools from the aggregator
734
+ target = namespace if namespace is not None else server_name
735
+ result = await self._aggregator.list_mcp_tools(target)
809
736
 
810
737
  # Apply filtering if tools are specified in config
811
738
  if self.config.tools is not None:
@@ -824,24 +751,15 @@ class BaseAgent(MCPAggregator, AgentProtocol):
824
751
  filtered_result[server] = filtered_tools
825
752
  result = filtered_result
826
753
 
827
- # Add human input tool to a special server if human input is configured
828
- if self.human_input_callback:
829
- from mcp.server.fastmcp.tools import Tool as FastTool
830
-
831
- human_input_tool: FastTool = FastTool.from_function(self.request_human_input)
754
+ # Add elicitation-backed human input tool to a special server if enabled and available
755
+ if self.config.human_input and getattr(self, "_human_input_tool", None):
832
756
  special_server_name = "__human_input__"
833
-
757
+
834
758
  # If the special server doesn't exist in result, create it
835
759
  if special_server_name not in result:
836
760
  result[special_server_name] = []
837
-
838
- result[special_server_name].append(
839
- Tool(
840
- name=HUMAN_INPUT_TOOL_NAME,
841
- description=human_input_tool.description,
842
- inputSchema=human_input_tool.parameters,
843
- )
844
- )
761
+
762
+ result[special_server_name].append(self._human_input_tool)
845
763
 
846
764
  return result
847
765
 
@@ -863,19 +781,93 @@ class BaseAgent(MCPAggregator, AgentProtocol):
863
781
  skills.append(await self.convert(tool))
864
782
 
865
783
  return AgentCard(
866
- name=self.name,
784
+ skills=skills,
785
+ name=self._name,
867
786
  description=self.instruction,
868
- url=f"fast-agent://agents/{self.name}/",
787
+ url=f"fast-agent://agents/{self._name}/",
869
788
  version="0.1",
870
789
  capabilities=DEFAULT_CAPABILITIES,
871
- defaultInputModes=["text/plain"],
872
- defaultOutputModes=["text/plain"],
790
+ default_input_modes=["text/plain"],
791
+ default_output_modes=["text/plain"],
873
792
  provider=None,
874
- documentationUrl=None,
875
- authentication=None,
876
- skills=skills,
793
+ documentation_url=None,
794
+ )
795
+
796
+ async def show_assistant_message(
797
+ self,
798
+ message: PromptMessageExtended,
799
+ bottom_items: List[str] | None = None,
800
+ highlight_items: str | List[str] | None = None,
801
+ max_item_length: int | None = None,
802
+ name: str | None = None,
803
+ model: str | None = None,
804
+ additional_message: Optional["Text"] = None,
805
+ ) -> None:
806
+ """
807
+ Display an assistant message with MCP servers in the bottom bar.
808
+
809
+ This override adds the list of connected MCP servers to the bottom bar
810
+ and highlights servers that were used for tool calls in this message.
811
+ """
812
+ # Get the list of MCP servers (if not provided)
813
+ if bottom_items is None:
814
+ if self._aggregator and self._aggregator.server_names:
815
+ server_names = self._aggregator.server_names
816
+ else:
817
+ server_names = []
818
+ else:
819
+ server_names = bottom_items
820
+
821
+ # Extract servers from tool calls in the message for highlighting
822
+ if highlight_items is None:
823
+ highlight_servers = self._extract_servers_from_message(message)
824
+ else:
825
+ # Convert to list if needed
826
+ if isinstance(highlight_items, str):
827
+ highlight_servers = [highlight_items]
828
+ else:
829
+ highlight_servers = highlight_items
830
+
831
+ # Call parent's implementation with server information
832
+ await super().show_assistant_message(
833
+ message=message,
834
+ bottom_items=server_names,
835
+ highlight_items=highlight_servers,
836
+ max_item_length=max_item_length or 12,
837
+ name=name,
838
+ model=model,
839
+ additional_message=additional_message,
877
840
  )
878
841
 
842
+ def _extract_servers_from_message(self, message: PromptMessageExtended) -> List[str]:
843
+ """
844
+ Extract server names from tool calls in the message.
845
+
846
+ Args:
847
+ message: The message containing potential tool calls
848
+
849
+ Returns:
850
+ List of server names that were called
851
+ """
852
+ servers = []
853
+
854
+ # Check if message has tool calls
855
+ if message.tool_calls:
856
+ for tool_request in message.tool_calls.values():
857
+ tool_name = tool_request.params.name
858
+
859
+ # Use aggregator's mapping to find the server for this tool
860
+ if tool_name in self._aggregator._namespaced_tool_map:
861
+ namespaced_tool = self._aggregator._namespaced_tool_map[tool_name]
862
+ if namespaced_tool.server_name not in servers:
863
+ servers.append(namespaced_tool.server_name)
864
+
865
+ return servers
866
+
867
+ async def _parse_resource_name(self, name: str, resource_type: str) -> tuple[str, str]:
868
+ """Delegate resource name parsing to the aggregator."""
869
+ return await self._aggregator._parse_resource_name(name, resource_type)
870
+
879
871
  async def convert(self, tool: Tool) -> AgentSkill:
880
872
  """
881
873
  Convert a Tool to an AgentSkill.
@@ -885,26 +877,26 @@ class BaseAgent(MCPAggregator, AgentProtocol):
885
877
  return AgentSkill(
886
878
  id=tool.name,
887
879
  name=tool_without_namespace,
888
- description=tool.description,
880
+ description=tool.description or "",
889
881
  tags=["tool"],
890
882
  examples=None,
891
- inputModes=None, # ["text/plain"],
883
+ input_modes=None, # ["text/plain"],
892
884
  # cover TextContent | ImageContent ->
893
885
  # https://github.com/modelcontextprotocol/modelcontextprotocol/pull/223
894
886
  # https://github.com/modelcontextprotocol/modelcontextprotocol/pull/93
895
- outputModes=None, # ,["text/plain", "image/*"],
887
+ output_modes=None, # ,["text/plain", "image/*"],
896
888
  )
897
889
 
898
890
  @property
899
- def message_history(self) -> List[PromptMessageMultipart]:
891
+ def message_history(self) -> List[PromptMessageExtended]:
900
892
  """
901
- Return the agent's message history as PromptMessageMultipart objects.
893
+ Return the agent's message history as PromptMessageExtended objects.
902
894
 
903
895
  This history can be used to transfer state between agents or for
904
896
  analysis and debugging purposes.
905
897
 
906
898
  Returns:
907
- List of PromptMessageMultipart objects representing the conversation history
899
+ List of PromptMessageExtended objects representing the conversation history
908
900
  """
909
901
  if self._llm:
910
902
  return self._llm.message_history