fast-agent-mcp 0.2.55__py3-none-any.whl → 0.2.57__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fast-agent-mcp
3
- Version: 0.2.55
3
+ Version: 0.2.57
4
4
  Summary: Define, Prompt and Test MCP enabled Agents and Workflows
5
5
  Author-email: Shaun Smith <fastagent@llmindset.co.uk>
6
6
  License: Apache License
@@ -211,7 +211,7 @@ Classifier: Programming Language :: Python :: 3
211
211
  Requires-Python: >=3.13
212
212
  Requires-Dist: a2a-sdk>=0.3.0
213
213
  Requires-Dist: aiohttp>=3.11.13
214
- Requires-Dist: anthropic>=0.59.0
214
+ Requires-Dist: anthropic>=0.63.0
215
215
  Requires-Dist: azure-identity>=1.14.0
216
216
  Requires-Dist: boto3>=1.35.0
217
217
  Requires-Dist: deprecated>=1.2.18
@@ -219,7 +219,7 @@ Requires-Dist: email-validator>=2.2.0
219
219
  Requires-Dist: fastapi>=0.115.6
220
220
  Requires-Dist: google-genai>=1.27.0
221
221
  Requires-Dist: mcp==1.12.4
222
- Requires-Dist: openai>=1.97.1
222
+ Requires-Dist: openai>=1.99.9
223
223
  Requires-Dist: opentelemetry-distro>=0.55b0
224
224
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.7.0
225
225
  Requires-Dist: opentelemetry-instrumentation-anthropic>=0.43.1; python_version >= '3.10' and python_version < '4.0'
@@ -234,20 +234,6 @@ Requires-Dist: pyyaml>=6.0.2
234
234
  Requires-Dist: rich>=14.1.0
235
235
  Requires-Dist: tensorzero>=2025.7.5
236
236
  Requires-Dist: typer>=0.15.1
237
- Provides-Extra: azure
238
- Requires-Dist: azure-identity>=1.14.0; extra == 'azure'
239
- Provides-Extra: dev
240
- Requires-Dist: anthropic>=0.42.0; extra == 'dev'
241
- Requires-Dist: pre-commit>=4.0.1; extra == 'dev'
242
- Requires-Dist: pydantic>=2.10.4; extra == 'dev'
243
- Requires-Dist: pytest-asyncio>=0.21.1; extra == 'dev'
244
- Requires-Dist: pytest-cov; extra == 'dev'
245
- Requires-Dist: pytest>=7.4.0; extra == 'dev'
246
- Requires-Dist: pyyaml>=6.0.2; extra == 'dev'
247
- Requires-Dist: ruff>=0.8.4; extra == 'dev'
248
- Requires-Dist: tomli>=2.2.1; extra == 'dev'
249
- Provides-Extra: openai
250
- Requires-Dist: openai>=1.58.1; extra == 'openai'
251
237
  Description-Content-Type: text/markdown
252
238
 
253
239
  <p align="center">
@@ -4,13 +4,13 @@ mcp_agent/config.py,sha256=BjfZmhKnoGHH80puLY90RkZPx5PPm-yvgJEqPsvvNWY,19259
4
4
  mcp_agent/console.py,sha256=Gjf2QLFumwG1Lav__c07X_kZxxEUSkzV-1_-YbAwcwo,813
5
5
  mcp_agent/context.py,sha256=lzz_Fyf9lz9BBAUt1bRVBlyyHjLkyeuyIziAi4qXYUk,7639
6
6
  mcp_agent/context_dependent.py,sha256=QXfhw3RaQCKfscEEBRGuZ3sdMWqkgShz2jJ1ivGGX1I,1455
7
- mcp_agent/event_progress.py,sha256=EdozJ6a13Ur_zMMbPx-1pKwsLEbHF0bwCEcErhxLCew,4513
7
+ mcp_agent/event_progress.py,sha256=YEk4RxHtblLNIn-ea8c9uHkEKOhtLEWOO662yjkf8Vo,4614
8
8
  mcp_agent/mcp_server_registry.py,sha256=lmz-aES-l7Gbg4itDF0iCmpso_KD8bVazVKSVzjwNE4,12398
9
9
  mcp_agent/progress_display.py,sha256=GeJU9VUt6qKsFVymG688hCMVCsAygG9ifiiEb5IcbN4,361
10
10
  mcp_agent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  mcp_agent/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- mcp_agent/agents/agent.py,sha256=EAYlcP1qqI1D0_CS808I806z1048FBjZQxxpcCZPeIU,3154
13
- mcp_agent/agents/base_agent.py,sha256=4hyEdJJENxIDLCtYOAmRlmM7dRVE0JPEhOYktPTw1I0,34566
12
+ mcp_agent/agents/agent.py,sha256=-vAbAXMt_0jIXyU1_rvcbGTCjwT3I0wyFL7kAdxI1Qk,3361
13
+ mcp_agent/agents/base_agent.py,sha256=IVQNCJMgs9gfUnhS6BvK1bVEe0ZfjzZXmWKbpI980i0,34875
14
14
  mcp_agent/agents/workflow/__init__.py,sha256=HloteEW6kalvgR0XewpiFAqaQlMPlPJYg5p3K33IUzI,25
15
15
  mcp_agent/agents/workflow/chain_agent.py,sha256=eIlImirrSXkqBJmPuAJgOKis81Cl6lZEGM0-6IyaUV8,6105
16
16
  mcp_agent/agents/workflow/evaluator_optimizer.py,sha256=LT81m2B7fxgBZY0CorXFOZJbVhM5fnjDjfrcywO5UrM,12210
@@ -26,25 +26,25 @@ mcp_agent/cli/constants.py,sha256=KawdkaN289nVB02DKPB4IVUJ8-fohIUD0gLfOp0P7B8,55
26
26
  mcp_agent/cli/main.py,sha256=Oo13X7LB0Cf7JrkilQXz8Eqi_48cE0Rr2qqDUOQifEQ,3175
27
27
  mcp_agent/cli/terminal.py,sha256=GRwD-RGW7saIz2IOWZn5vD6JjiArscELBThm1GTFkuI,1065
28
28
  mcp_agent/cli/commands/check_config.py,sha256=15YK0mtDQbVopnMm3HBjOeY2-00FUHj6tt8RvaemKmI,21081
29
- mcp_agent/cli/commands/go.py,sha256=SHFJlO3SeZzAWvx_Gz72zXKA8n7XfaZ49W6UNS5mbV4,14668
29
+ mcp_agent/cli/commands/go.py,sha256=RfNJQYmuDJicpYM7oT-jfv_9P7kBqCh_5Ezd4RtcYME,14712
30
30
  mcp_agent/cli/commands/quickstart.py,sha256=hwIr1F9zGRSQGf7kwvmirMIK7Qke2s6W95inZ-a2SMQ,21171
31
31
  mcp_agent/cli/commands/server_helpers.py,sha256=x5tD_qhf1W4D2li09sfOyfRWCOCa6lmpumYAPsEfIQs,3649
32
32
  mcp_agent/cli/commands/setup.py,sha256=eOEd4TL-b0DaDeSJMGOfNOsTEItoZ67W88eTP4aP-bo,6482
33
33
  mcp_agent/cli/commands/url_parser.py,sha256=5VdtcHRHzi67YignStVbz7u-rcvNNErw9oJLAUFOtEY,5855
34
34
  mcp_agent/core/__init__.py,sha256=7xAVBbyrsJwmrzL-we9tBY9PcJxFnajoJcaXSc6gBUw,391
35
- mcp_agent/core/agent_app.py,sha256=SolGwejEmv9XtsTsmiMkNKPia7RN1VcHXm6JoEo4hvQ,16187
35
+ mcp_agent/core/agent_app.py,sha256=2p-wlQZJ7LNFOIxDdzt7u2p0ar-5To0laiSBCNaZWpw,16834
36
36
  mcp_agent/core/agent_types.py,sha256=ClFN7ikstZ5suweeLFf5w9jcg3ygxVhMS7GRj9VJ8yQ,1749
37
37
  mcp_agent/core/direct_decorators.py,sha256=mOMLHf_qhWFqcOY0QE1UbHtXAarnetIKp7n0U8WvcyM,24061
38
38
  mcp_agent/core/direct_factory.py,sha256=0G1HbVECfdFJGn__lBYGha_qt7Xyp18ST18IOzO134E,20867
39
- mcp_agent/core/enhanced_prompt.py,sha256=ZIeJCeW7rcGMBZ2OdEQwqOmRT0wNSp0hO2-dZRSnnLE,36068
39
+ mcp_agent/core/enhanced_prompt.py,sha256=QeBajgqu09SyK0ULEzsauAM-xlR4oLtT23udbZzVLuk,36410
40
40
  mcp_agent/core/error_handling.py,sha256=xoyS2kLe0eG0bj2eSJCJ2odIhGUve2SbDR7jP-A-uRw,624
41
41
  mcp_agent/core/exceptions.py,sha256=ENAD_qGG67foxy6vDkIvc-lgopIUQy6O7zvNPpPXaQg,2289
42
- mcp_agent/core/fastagent.py,sha256=HiKuxHsj0wq6_RV0wkJih8br39ciHNnnfVEsknhbiQA,25264
42
+ mcp_agent/core/fastagent.py,sha256=dYN2fpgDSrk7iGHN7JmW0e8eFF47oqDmatzdUHz0eEI,25306
43
43
  mcp_agent/core/interactive_prompt.py,sha256=Gpwn7v7m3nw7fR_W7bs5XYArrny_xJK06DOURUwpzI4,32756
44
44
  mcp_agent/core/mcp_content.py,sha256=2D7KHY9mG_vxoDwFLKvsPQV9VRIzHItM7V-jcEnACh8,8878
45
45
  mcp_agent/core/mermaid_utils.py,sha256=MpcRyVCPMTwU1XeIxnyFg0fQLjcyXZduWRF8NhEqvXE,5332
46
46
  mcp_agent/core/prompt.py,sha256=qnintOUGEoDPYLI9bu9G2OlgVMCe5ZPUZilgMzydXhc,7919
47
- mcp_agent/core/request_params.py,sha256=qmFWZXeYEJyYw2IwonyrTnZWxQG7qX6bKpOPcqETa60,1603
47
+ mcp_agent/core/request_params.py,sha256=OW9WnQAD-I2fz2JzMsqPY2wwwHFG0SI4yAvC1WxTfNY,1735
48
48
  mcp_agent/core/usage_display.py,sha256=CcCQbTm5ftMgkoBgr5NWzHmYFBK5cSU-FM8yiBOVzGA,7527
49
49
  mcp_agent/core/validation.py,sha256=8D6d3mQanvzC2dXx5yc8-5bkoWEh9cxuT6-YSthSGFk,12676
50
50
  mcp_agent/executor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -61,7 +61,7 @@ mcp_agent/human_input/handler.py,sha256=s712Z5ssTCwjL9-VKoIdP5CtgMh43YvepynYisiW
61
61
  mcp_agent/human_input/simple_form.py,sha256=u6iCo39IJftB1S1xljdQP3C18RuRCcwp7jKQTTDcLT4,3441
62
62
  mcp_agent/human_input/types.py,sha256=RtWBOVzy8vnYoQrc36jRLn8z8N3C4pDPMBN5vF6qM5Y,1476
63
63
  mcp_agent/llm/__init__.py,sha256=d8zgwG-bRFuwiMNMYkywg_qytk4P8lawyld_meuUmHI,68
64
- mcp_agent/llm/augmented_llm.py,sha256=UCAKqlsv3eUBIhOz0p3_bSNKOY3MykJIb7OBKoXJjWI,27973
64
+ mcp_agent/llm/augmented_llm.py,sha256=ZJM3TXGV66aQ_zSDPrOG0r6EMVOOdeh1Equq8uNjDBM,28642
65
65
  mcp_agent/llm/augmented_llm_passthrough.py,sha256=bu0DJkjyFPzBZEU7f6MHnOp__9BCYl56tFd5nZVhSeY,8808
66
66
  mcp_agent/llm/augmented_llm_playback.py,sha256=rLzgai496e2RlxqQp_Bp0U-Y1FF1SGsWl9COx4GiCNE,5004
67
67
  mcp_agent/llm/augmented_llm_silent.py,sha256=IUnK_1Byy4D9TG0Pj46LFeNezgSTQ8d6MQIHWAImBwE,1846
@@ -80,13 +80,13 @@ mcp_agent/llm/providers/anthropic_utils.py,sha256=vYDN5G5jKMhD2CQg8veJYab7tvvzYk
80
80
  mcp_agent/llm/providers/augmented_llm_aliyun.py,sha256=Th4qeyihOSfTrojPw8YsgzEyaDwhybk7hkXkFjH-1dY,1277
81
81
  mcp_agent/llm/providers/augmented_llm_anthropic.py,sha256=U8znA9HSCOx4FlhIjl-J29nbYc8Y-T6BXevtXuDafio,30352
82
82
  mcp_agent/llm/providers/augmented_llm_azure.py,sha256=Xoo6dFst6L9SaGKurqptwwTUzr-sYsolZ-AFb_79puc,6098
83
- mcp_agent/llm/providers/augmented_llm_bedrock.py,sha256=nfby1udL07zPTOLlN_tFxd1h0JRioo2oIW7v4iP4Bnk,82267
83
+ mcp_agent/llm/providers/augmented_llm_bedrock.py,sha256=lL_au5-QdVNFr7lxQQYk0HVQ3LKUieh2HHnBQsOWoro,82312
84
84
  mcp_agent/llm/providers/augmented_llm_deepseek.py,sha256=vphkYMFyukaejBw8SkCN-MqcG9qsfXJfWKZYYtspqPY,3877
85
85
  mcp_agent/llm/providers/augmented_llm_generic.py,sha256=5Uq8ZBhcFuQTt7koP_5ykolREh2iWu8zKhNbh3pM9lQ,1210
86
86
  mcp_agent/llm/providers/augmented_llm_google_native.py,sha256=c6zczfs-Iw70j3OYELHJ4S7CRwAddkeXinex_yLMhmU,22194
87
87
  mcp_agent/llm/providers/augmented_llm_google_oai.py,sha256=g_g46h-YuxqbRZiO_dVo5zO2OkX1yx7nb6xDaQbOvWs,1137
88
- mcp_agent/llm/providers/augmented_llm_groq.py,sha256=Nj4X7wL7nOz_JYC-3bFOWut-hleZxGqWHNe0U-0ga8Y,4419
89
- mcp_agent/llm/providers/augmented_llm_openai.py,sha256=d5WeCU6smP7a5egZOBpblF62mK5kOzK4tn2YeQoK34A,25401
88
+ mcp_agent/llm/providers/augmented_llm_groq.py,sha256=w-R1N2B7790v9tXxRCPt2SKnaB-DRdEtN_51fpdYb_I,5355
89
+ mcp_agent/llm/providers/augmented_llm_openai.py,sha256=v0iInSlPt939Dh2kGpGJ9_7qGpL7MhxQ7UNSc42dT6Q,25318
90
90
  mcp_agent/llm/providers/augmented_llm_openrouter.py,sha256=m3wS83fabBOmaZJH9gQ9sFw_2TB4xTb44WCOPB-2NJ4,2001
91
91
  mcp_agent/llm/providers/augmented_llm_tensorzero_openai.py,sha256=D53Fry2AfBLOB5z9hM19U6_HMJeVNTpiBCAJb11ulNg,5503
92
92
  mcp_agent/llm/providers/augmented_llm_xai.py,sha256=MhlX91IUNynQ_NDknx4EQJLwg-NbR8lcHS1P4JuLOnA,1433
@@ -112,9 +112,9 @@ mcp_agent/mcp/gen_client.py,sha256=fAVwFVCgSamw4PwoWOV4wrK9TABx1S_zZv8BctRyF2k,3
112
112
  mcp_agent/mcp/hf_auth.py,sha256=7szw4rkwRyK3J-sUTcVZHdwoLIZqlYo8XolJnZdjOww,4571
113
113
  mcp_agent/mcp/interfaces.py,sha256=5y7zuXkpGCfsJfZOAEZAZtbFd5rAKjn7oG4JoblPHZ4,8070
114
114
  mcp_agent/mcp/logger_textio.py,sha256=vljC1BtNTCxBAda9ExqNB-FwVNUZIuJT3h1nWmCjMws,3172
115
- mcp_agent/mcp/mcp_agent_client_session.py,sha256=ssRgxMOzyCPyAr25AeRBz3Xr-08OBdmsJf8A-ofc3bY,9115
116
- mcp_agent/mcp/mcp_aggregator.py,sha256=6fZTgoiUfE7YHjLug1ZQ2VtKYj8HVe_diMwNN6npPGk,53343
117
- mcp_agent/mcp/mcp_connection_manager.py,sha256=dJxjnv2IRzlFIxrbPFl39-pmGcZHgyeMXVlMfqpREhE,17974
115
+ mcp_agent/mcp/mcp_agent_client_session.py,sha256=WUx-9usO6PHwyfodIPEtd88DNDU-1YDdvQ4TNsEpfqI,13383
116
+ mcp_agent/mcp/mcp_aggregator.py,sha256=9r0EOrb8UR9br6qrjwW4deWWO2Jt7NFrYJenhrePRAk,55915
117
+ mcp_agent/mcp/mcp_connection_manager.py,sha256=VaWH-PWjVfaCDEWT0nfMblJ_3BbvDqDgoeMtGuLS9_M,18716
118
118
  mcp_agent/mcp/mime_utils.py,sha256=difepNR_gpb4MpMLkBRAoyhDk-AjXUHTiqKvT_VwS1o,1805
119
119
  mcp_agent/mcp/prompt_message_multipart.py,sha256=-oSO0mnc5gkSgulE1gAntPEwAKF4asOjEeVyLjhYrEk,4336
120
120
  mcp_agent/mcp/prompt_render.py,sha256=k3v4BZDThGE2gGiOYVQtA6x8WTEdOuXIEnRafANhN1U,2996
@@ -183,10 +183,10 @@ mcp_agent/resources/examples/workflows/router.py,sha256=vmw8aBitByi5PRFIvjYWWn2G
183
183
  mcp_agent/resources/examples/workflows/short_story.md,sha256=XN9I2kzCcMmke3dE5F2lyRH5iFUZUQ8Sy-hS3rm_Wlc,1153
184
184
  mcp_agent/resources/examples/workflows/short_story.txt,sha256=X3y_1AyhLFN2AKzCKvucJtDgAFIJfnlbsbGZO5bBWu0,1187
185
185
  mcp_agent/tools/tool_definition.py,sha256=L3Pxl-uLEXqlVoo-bYuFTFALeI-2pIU44YgFhsTKEtM,398
186
- mcp_agent/ui/console_display.py,sha256=XXrHr950wSBSedEKUaaGkXjOzuFpQYzUKKiyaZ58Mps,28280
186
+ mcp_agent/ui/console_display.py,sha256=ts2oPxxprzbT9rMqZhzwq6DQkNfbq8XNwbgEPPMi7nI,32083
187
187
  mcp_agent/ui/console_display_legacy.py,sha256=sm2v61-IPVafbF7uUaOyhO2tW_zgFWOjNS83IEWqGgI,14931
188
- fast_agent_mcp-0.2.55.dist-info/METADATA,sha256=wJSX8ZuwOm6h7kbWev3_cv1CQRDHT0C60xwlE9tXPqE,31048
189
- fast_agent_mcp-0.2.55.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
190
- fast_agent_mcp-0.2.55.dist-info/entry_points.txt,sha256=QaX5kLdI0VdMPRdPUF1nkG_WdLUTNjp_icW6e3EhNYU,232
191
- fast_agent_mcp-0.2.55.dist-info/licenses/LICENSE,sha256=Gx1L3axA4PnuK4FxsbX87jQ1opoOkSFfHHSytW6wLUU,10935
192
- fast_agent_mcp-0.2.55.dist-info/RECORD,,
188
+ fast_agent_mcp-0.2.57.dist-info/METADATA,sha256=8NJird91GzBusg3XcllAlxSRvG-fHS9KR7A2AFJowhw,30459
189
+ fast_agent_mcp-0.2.57.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
190
+ fast_agent_mcp-0.2.57.dist-info/entry_points.txt,sha256=QaX5kLdI0VdMPRdPUF1nkG_WdLUTNjp_icW6e3EhNYU,232
191
+ fast_agent_mcp-0.2.57.dist-info/licenses/LICENSE,sha256=Gx1L3axA4PnuK4FxsbX87jQ1opoOkSFfHHSytW6wLUU,10935
192
+ fast_agent_mcp-0.2.57.dist-info/RECORD,,
mcp_agent/agents/agent.py CHANGED
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, TypeVar
10
10
  from mcp_agent.agents.base_agent import BaseAgent
11
11
  from mcp_agent.core.agent_types import AgentConfig
12
12
  from mcp_agent.core.interactive_prompt import InteractivePrompt
13
+ from mcp_agent.core.request_params import RequestParams
13
14
  from mcp_agent.human_input.types import HumanInputCallback
14
15
  from mcp_agent.logging.logger import get_logger
15
16
  from mcp_agent.mcp.interfaces import AugmentedLLMProtocol
@@ -51,13 +52,19 @@ class Agent(BaseAgent):
51
52
  **kwargs,
52
53
  )
53
54
 
54
- async def prompt(self, default_prompt: str = "", agent_name: Optional[str] = None) -> str:
55
+ async def prompt(
56
+ self,
57
+ default_prompt: str = "",
58
+ agent_name: Optional[str] = None,
59
+ request_params: RequestParams | None = None
60
+ ) -> str:
55
61
  """
56
62
  Start an interactive prompt session with this agent.
57
63
 
58
64
  Args:
59
65
  default: Default message to use when user presses enter
60
66
  agent_name: Ignored for single agents, included for API compatibility
67
+ request_params: Optional request parameters
61
68
 
62
69
  Returns:
63
70
  The result of the interactive session
@@ -66,14 +73,14 @@ class Agent(BaseAgent):
66
73
  agent_name_str = str(self.name)
67
74
 
68
75
  # Create agent_types dictionary with just this agent
69
- agent_types = {agent_name_str: self.agent_type.value}
76
+ agent_types = {agent_name_str: self.agent_type}
70
77
 
71
78
  # Create the interactive prompt
72
79
  prompt = InteractivePrompt(agent_types=agent_types)
73
80
 
74
81
  # Define wrapper for send function
75
82
  async def send_wrapper(message, agent_name):
76
- return await self.send(message)
83
+ return await self.send(message, request_params)
77
84
 
78
85
  # Start the prompt loop with just this agent
79
86
  return await prompt.prompt_loop(
@@ -208,7 +208,11 @@ class BaseAgent(MCPAggregator, AgentProtocol):
208
208
  result: PromptMessageMultipart = await self.generate([Prompt.user(message)], request_params)
209
209
  return result.first_text()
210
210
 
211
- async def send(self, message: Union[str, PromptMessage, PromptMessageMultipart]) -> str:
211
+ async def send(
212
+ self,
213
+ message: Union[str, PromptMessage, PromptMessageMultipart],
214
+ request_params: RequestParams | None = None
215
+ ) -> str:
212
216
  """
213
217
  Send a message to the agent and get a response.
214
218
 
@@ -217,6 +221,7 @@ class BaseAgent(MCPAggregator, AgentProtocol):
217
221
  - String: Converted to a user PromptMessageMultipart
218
222
  - PromptMessage: Converted to PromptMessageMultipart
219
223
  - PromptMessageMultipart: Used directly
224
+ - request_params: Optional request parameters
220
225
 
221
226
  Returns:
222
227
  The agent's response as a string
@@ -225,7 +230,7 @@ class BaseAgent(MCPAggregator, AgentProtocol):
225
230
  prompt = self._normalize_message_input(message)
226
231
 
227
232
  # Use the LLM to generate a response
228
- response = await self.generate([prompt], None)
233
+ response = await self.generate([prompt], request_params)
229
234
  return response.all_text()
230
235
 
231
236
  def _normalize_message_input(
@@ -364,12 +369,15 @@ class BaseAgent(MCPAggregator, AgentProtocol):
364
369
  if self.config.tools is not None:
365
370
  filtered_tools = []
366
371
  for tool in result.tools:
367
- # Extract server name from tool name (e.g., "mathematics-add" -> "mathematics")
368
- if "-" in tool.name:
369
- server_name = tool.name.split("-", 1)[0]
372
+ # Extract server name from tool name, handling server names with hyphens
373
+ server_name = None
374
+ for configured_server in self.config.tools.keys():
375
+ if tool.name.startswith(f"{configured_server}-"):
376
+ server_name = configured_server
377
+ break
370
378
 
371
- # Check if this server has tool filters
372
- if server_name in self.config.tools:
379
+ # Check if this server has tool filters
380
+ if server_name and server_name in self.config.tools:
373
381
  # Check if tool matches any pattern for this server
374
382
  for pattern in self.config.tools[server_name]:
375
383
  if self._matches_pattern(tool.name, pattern, server_name):
@@ -121,9 +121,9 @@ async def _run_agent(
121
121
  print(response)
122
122
  elif prompt_file:
123
123
  prompt = load_prompt_multipart(Path(prompt_file))
124
- response = await agent.generate(prompt)
125
- # Print the response text and exit
126
- print(response.last_text())
124
+ response = await agent.agent.generate(prompt)
125
+ print(f"\nLoaded {len(prompt)} messages from prompt file '{prompt_file}'")
126
+ await agent.interactive()
127
127
  else:
128
128
  await agent.interactive()
129
129
 
@@ -11,6 +11,7 @@ from rich import print as rich_print
11
11
  from mcp_agent.agents.agent import Agent
12
12
  from mcp_agent.core.agent_types import AgentType
13
13
  from mcp_agent.core.interactive_prompt import InteractivePrompt
14
+ from mcp_agent.core.request_params import RequestParams
14
15
  from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
15
16
  from mcp_agent.progress_display import progress_display
16
17
 
@@ -53,6 +54,7 @@ class AgentApp:
53
54
  message: Union[str, PromptMessage, PromptMessageMultipart] | None = None,
54
55
  agent_name: str | None = None,
55
56
  default_prompt: str = "",
57
+ request_params: RequestParams | None = None,
56
58
  ) -> str:
57
59
  """
58
60
  Make the object callable to send messages or start interactive prompt.
@@ -65,19 +67,21 @@ class AgentApp:
65
67
  - PromptMessageMultipart: Used directly
66
68
  agent_name: Optional name of the agent to send to (defaults to first agent)
67
69
  default_prompt: Default message to use in interactive prompt mode
70
+ request_params: Optional request parameters including MCP metadata
68
71
 
69
72
  Returns:
70
73
  The agent's response as a string or the result of the interactive session
71
74
  """
72
75
  if message:
73
- return await self._agent(agent_name).send(message)
76
+ return await self._agent(agent_name).send(message, request_params)
74
77
 
75
- return await self.interactive(agent_name=agent_name, default_prompt=default_prompt)
78
+ return await self.interactive(agent_name=agent_name, default_prompt=default_prompt, request_params=request_params)
76
79
 
77
80
  async def send(
78
81
  self,
79
82
  message: Union[str, PromptMessage, PromptMessageMultipart],
80
83
  agent_name: Optional[str] = None,
84
+ request_params: RequestParams | None = None,
81
85
  ) -> str:
82
86
  """
83
87
  Send a message to the specified agent (or to all agents).
@@ -88,11 +92,12 @@ class AgentApp:
88
92
  - PromptMessage: Converted to PromptMessageMultipart
89
93
  - PromptMessageMultipart: Used directly
90
94
  agent_name: Optional name of the agent to send to
95
+ request_params: Optional request parameters including MCP metadata
91
96
 
92
97
  Returns:
93
98
  The agent's response as a string
94
99
  """
95
- return await self._agent(agent_name).send(message)
100
+ return await self._agent(agent_name).send(message, request_params)
96
101
 
97
102
  def _agent(self, agent_name: str | None) -> Agent:
98
103
  if agent_name:
@@ -233,17 +238,23 @@ class AgentApp:
233
238
  )
234
239
 
235
240
  @deprecated
236
- async def prompt(self, agent_name: str | None = None, default_prompt: str = "") -> str:
241
+ async def prompt(
242
+ self,
243
+ agent_name: str | None = None,
244
+ default_prompt: str = "",
245
+ request_params: RequestParams | None = None
246
+ ) -> str:
237
247
  """
238
248
  Deprecated - use interactive() instead.
239
249
  """
240
- return await self.interactive(agent_name=agent_name, default_prompt=default_prompt)
250
+ return await self.interactive(agent_name=agent_name, default_prompt=default_prompt, request_params=request_params)
241
251
 
242
252
  async def interactive(
243
253
  self,
244
254
  agent_name: str | None = None,
245
255
  default_prompt: str = "",
246
256
  pretty_print_parallel: bool = False,
257
+ request_params: RequestParams | None = None,
247
258
  ) -> str:
248
259
  """
249
260
  Interactive prompt for sending messages with advanced features.
@@ -252,6 +263,7 @@ class AgentApp:
252
263
  agent_name: Optional target agent name (uses default if not specified)
253
264
  default: Default message to use when user presses enter
254
265
  pretty_print_parallel: Enable clean parallel results display for parallel agents
266
+ request_params: Optional request parameters including MCP metadata
255
267
 
256
268
  Returns:
257
269
  The result of the interactive session
@@ -285,7 +297,7 @@ class AgentApp:
285
297
 
286
298
  # Define the wrapper for send function
287
299
  async def send_wrapper(message, agent_name):
288
- result = await self.send(message, agent_name)
300
+ result = await self.send(message, agent_name, request_params)
289
301
 
290
302
  # Show parallel results if enabled and this is a parallel agent
291
303
  if pretty_print_parallel:
@@ -428,9 +428,14 @@ def create_keybindings(on_toggle_multiline=None, app=None, agent_provider=None,
428
428
  """Enter: accept input when not in multiline mode."""
429
429
  event.current_buffer.validate_and_handle()
430
430
 
431
+ @kb.add("c-j", filter=Condition(lambda: not in_multiline_mode))
432
+ def _(event) -> None:
433
+ """Ctrl+J: Insert newline when in normal mode."""
434
+ event.current_buffer.insert_text("\n")
435
+
431
436
  @kb.add("c-m", filter=Condition(lambda: in_multiline_mode))
432
437
  def _(event) -> None:
433
- """Enter: insert newline when in multiline mode."""
438
+ """Enter: Insert newline when in multiline mode."""
434
439
  event.current_buffer.insert_text("\n")
435
440
 
436
441
  # Use c-j (Ctrl+J) as an alternative to represent Ctrl+Enter in multiline mode
@@ -581,13 +586,15 @@ async def get_enhanced_input(
581
586
 
582
587
  shortcuts = [
583
588
  ("Ctrl+T", toggle_text),
589
+ ("Ctrl+J", "Newline" if not in_multiline_mode else None),
584
590
  ("Ctrl+E", "External"),
585
591
  ("Ctrl+Y", "Copy"),
586
592
  ("Ctrl+L", "Clear"),
587
593
  ("↑/↓", "History"),
594
+ ("EXIT", "Exit")
588
595
  ]
589
596
 
590
- newline = "Ctrl+&lt;Enter&gt;:Submit" if in_multiline_mode else "&lt;Enter&gt;:Submit"
597
+ newline = "Ctrl+J:Submit" if in_multiline_mode else "&lt;Enter&gt;:Submit"
591
598
 
592
599
  # Only show relevant shortcuts based on mode
593
600
  shortcuts = [(k, v) for k, v in shortcuts if v]
@@ -669,6 +676,8 @@ async def get_enhanced_input(
669
676
  def pre_process_input(text):
670
677
  # Command processing
671
678
  if text and text.startswith("/"):
679
+ if text == "/":
680
+ return ""
672
681
  cmd_parts = text[1:].strip().split(maxsplit=1)
673
682
  cmd = cmd_parts[0].lower()
674
683
 
@@ -87,6 +87,7 @@ class FastAgent:
87
87
  ignore_unknown_args: bool = False,
88
88
  parse_cli_args: bool = True,
89
89
  quiet: bool = False, # Add quiet parameter
90
+ **kwargs
90
91
  ) -> None:
91
92
  """
92
93
  Initialize the fast-agent application.
@@ -203,6 +204,7 @@ class FastAgent:
203
204
  self.app = MCPApp(
204
205
  name=name,
205
206
  settings=config.Settings(**self.config) if hasattr(self, "config") else None,
207
+ **kwargs
206
208
  )
207
209
 
208
210
  # Stop progress display immediately if quiet mode is requested
@@ -52,3 +52,8 @@ class RequestParams(CreateMessageRequestParams):
52
52
  """
53
53
  Optional dictionary of template variables for dynamic templates. Currently only works for TensorZero inference backend
54
54
  """
55
+
56
+ mcp_metadata: Dict[str, Any] | None = None
57
+ """
58
+ Metadata to pass through to MCP tool calls via the _meta field.
59
+ """
@@ -25,6 +25,9 @@ class ProgressAction(str, Enum):
25
25
  FINISHED = "Finished"
26
26
  SHUTDOWN = "Shutdown"
27
27
  AGGREGATOR_INITIALIZED = "Running"
28
+ SERVER_OFFLINE = "Offline"
29
+ SERVER_RECONNECTING = "Reconnecting"
30
+ SERVER_ONLINE = "Online"
28
31
  FATAL_ERROR = "Error"
29
32
 
30
33
 
@@ -1,4 +1,5 @@
1
1
  from abc import abstractmethod
2
+ from contextvars import ContextVar
2
3
  from typing import (
3
4
  TYPE_CHECKING,
4
5
  Any,
@@ -61,6 +62,9 @@ if TYPE_CHECKING:
61
62
  # TODO -- move this to a constant
62
63
  HUMAN_INPUT_TOOL_NAME = "__human_input__"
63
64
 
65
+ # Context variable for storing MCP metadata
66
+ _mcp_metadata_var: ContextVar[Dict[str, Any] | None] = ContextVar('mcp_metadata', default=None)
67
+
64
68
 
65
69
  def deep_merge(dict1: Dict[Any, Any], dict2: Dict[Any, Any]) -> Dict[Any, Any]:
66
70
  """
@@ -232,6 +236,11 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
232
236
 
233
237
  self._precall(multipart_messages)
234
238
 
239
+ # Store MCP metadata in context variable
240
+ final_request_params = self.get_request_params(request_params)
241
+ if final_request_params.mcp_metadata:
242
+ _mcp_metadata_var.set(final_request_params.mcp_metadata)
243
+
235
244
  assistant_response: PromptMessageMultipart = await self._apply_prompt_provider_specific(
236
245
  multipart_messages, request_params
237
246
  )
@@ -275,6 +284,12 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
275
284
  multipart_messages = PromptMessageMultipart.to_multipart(multipart_messages)
276
285
 
277
286
  self._precall(multipart_messages)
287
+
288
+ # Store MCP metadata in context variable
289
+ final_request_params = self.get_request_params(request_params)
290
+ if final_request_params.mcp_metadata:
291
+ _mcp_metadata_var.set(final_request_params.mcp_metadata)
292
+
278
293
  result, assistant_response = await self._apply_prompt_provider_specific_structured(
279
294
  multipart_messages, model, request_params
280
295
  )
@@ -86,6 +86,7 @@ class BedrockAugmentedLLM(AugmentedLLM[BedrockMessageParam, BedrockMessage]):
86
86
  r"^cohere\..*", # Cohere models
87
87
  r"^ai21\..*", # AI21 models
88
88
  r"^stability\..*", # Stability AI models
89
+ r"^openai\..*", # OpenAI models
89
90
  ]
90
91
 
91
92
  import re
@@ -49,35 +49,22 @@ class GroqAugmentedLLM(OpenAIAugmentedLLM):
49
49
  if "object" == json_mode:
50
50
  request_params.response_format = {"type": "json_object"}
51
51
 
52
- # Get the full schema and extract just the properties
53
- full_schema = model.model_json_schema()
54
- properties = full_schema.get("properties", {})
55
- required_fields = full_schema.get("required", [])
56
-
57
- # Create a cleaner format description
58
- format_description = "{\n"
59
- for field_name, field_info in properties.items():
60
- field_type = field_info.get("type", "string")
61
- description = field_info.get("description", "")
62
- format_description += f' "{field_name}": "{field_type}"'
63
- if description:
64
- format_description += f" // {description}"
65
- if field_name in required_fields:
66
- format_description += " // REQUIRED"
67
- format_description += "\n"
68
- format_description += "}"
69
-
70
- multipart_messages[-1].add_text(
71
- f"""YOU MUST RESPOND WITH A JSON OBJECT IN EXACTLY THIS FORMAT:
72
- {format_description}
73
-
74
- IMPORTANT RULES:
75
- - Respond ONLY with the JSON object, no other text
76
- - Do NOT include "properties" or "schema" wrappers
77
- - Do NOT use code fences or markdown
78
- - The response must be valid JSON that matches the format above
79
- - All required fields must be included"""
80
- )
52
+ # Create a cleaner format description from full schema
53
+ full_schema = model.model_json_schema()
54
+ format_description = self._schema_to_json_object(
55
+ full_schema, full_schema.get("$defs")
56
+ )
57
+
58
+ multipart_messages[-1].add_text(
59
+ f"""YOU MUST RESPOND WITH A JSON OBJECT IN EXACTLY THIS FORMAT:
60
+ {format_description}
61
+
62
+ IMPORTANT RULES:
63
+ - Respond ONLY with the JSON object, no other text
64
+ - Do NOT include "properties" or "schema" wrappers
65
+ - Do NOT use code fences or markdown
66
+ - The response must be valid JSON that matches the format above
67
+ - All required fields must be included""")
81
68
 
82
69
  result: PromptMessageMultipart = await self._apply_prompt_provider_specific(
83
70
  multipart_messages, request_params
@@ -101,3 +88,45 @@ class GroqAugmentedLLM(OpenAIAugmentedLLM):
101
88
  base_url = self.context.config.groq.base_url
102
89
 
103
90
  return base_url if base_url else GROQ_BASE_URL
91
+
92
+ def _schema_to_json_object(
93
+ self, schema: dict, defs: dict | None = None, visited: set | None = None
94
+ ) -> str:
95
+ visited = visited or set()
96
+
97
+ if id(schema) in visited:
98
+ return '"<recursive>"'
99
+ visited.add(id(schema))
100
+
101
+ if "$ref" in schema:
102
+ ref = schema.get("$ref", "")
103
+ if ref.startswith("#/$defs/"):
104
+ target = ref.split("/")[-1]
105
+ if defs and target in defs:
106
+ return self._schema_to_json_object(defs[target], defs, visited)
107
+ return f'"<ref:{ref}>"'
108
+
109
+ schema_type = schema.get("type")
110
+ description = schema.get("description", "")
111
+ required = schema.get("required", [])
112
+
113
+ if schema_type == "object":
114
+ props = schema.get("properties", {})
115
+ result = "{\n"
116
+ for prop_name, prop_schema in props.items():
117
+ is_required = prop_name in required
118
+ prop_str = self._schema_to_json_object(prop_schema, defs, visited)
119
+ if is_required:
120
+ prop_str += " // REQUIRED"
121
+ result += f' "{prop_name}": {prop_str},\n'
122
+ result += "}"
123
+ return result
124
+ elif schema_type == "array":
125
+ items = schema.get("items", {})
126
+ items_str = self._schema_to_json_object(items, defs, visited)
127
+ return f"[{items_str}]"
128
+ elif schema_type:
129
+ comment = f" // {description}" if description else ""
130
+ return f'"{schema_type}"' + comment
131
+
132
+ return '"<unknown>"'
@@ -171,7 +171,6 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
171
171
  async def _process_stream_manual(self, stream, model: str):
172
172
  """Manual stream processing for providers like Ollama that may not work with ChatCompletionStreamState."""
173
173
  from openai.types.chat import ChatCompletionMessageToolCall
174
- from openai.types.chat.chat_completion_message_tool_call import Function
175
174
 
176
175
  # Track estimated output tokens by counting text chunks
177
176
  estimated_tokens = 0
@@ -249,10 +248,10 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
249
248
  ChatCompletionMessageToolCall(
250
249
  id=tool_call_data["id"],
251
250
  type=tool_call_data["type"],
252
- function=Function(
253
- name=tool_call_data["function"]["name"],
254
- arguments=tool_call_data["function"]["arguments"],
255
- ),
251
+ function={
252
+ "name": tool_call_data["function"]["name"],
253
+ "arguments": tool_call_data["function"]["arguments"],
254
+ },
256
255
  )
257
256
  )
258
257
 
@@ -14,8 +14,17 @@ from mcp.shared.session import (
14
14
  SendRequestT,
15
15
  )
16
16
  from mcp.types import (
17
+ CallToolRequest,
18
+ CallToolRequestParams,
19
+ CallToolResult,
20
+ GetPromptRequest,
21
+ GetPromptRequestParams,
22
+ GetPromptResult,
17
23
  Implementation,
18
24
  ListRootsResult,
25
+ ReadResourceRequest,
26
+ ReadResourceRequestParams,
27
+ ReadResourceResult,
19
28
  Root,
20
29
  ToolListChangedNotification,
21
30
  )
@@ -180,8 +189,17 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
180
189
  )
181
190
  return result
182
191
  except Exception as e:
183
- logger.error(f"send_request failed: {str(e)}")
184
- raise
192
+ # Handle connection errors cleanly
193
+ from anyio import ClosedResourceError
194
+
195
+ if isinstance(e, ClosedResourceError):
196
+ # Show clean offline message and convert to ConnectionError
197
+ from mcp_agent import console
198
+ console.console.print(f"[dim red]MCP server {self.session_server_name} offline[/dim red]")
199
+ raise ConnectionError(f"MCP server {self.session_server_name} offline") from e
200
+ else:
201
+ logger.error(f"send_request failed: {str(e)}")
202
+ raise
185
203
 
186
204
  async def _received_notification(self, notification: ServerNotification) -> None:
187
205
  """
@@ -226,3 +244,88 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
226
244
  await self._tool_list_changed_callback(server_name)
227
245
  except Exception as e:
228
246
  logger.error(f"Error in tool list changed callback: {e}")
247
+
248
+ async def call_tool(
249
+ self,
250
+ name: str,
251
+ arguments: dict | None = None,
252
+ _meta: dict | None = None,
253
+ **kwargs
254
+ ) -> CallToolResult:
255
+ """Call a tool with optional metadata support."""
256
+ if _meta:
257
+ from mcp.types import RequestParams
258
+
259
+ # Safe merge - preserve existing meta fields like progressToken
260
+ existing_meta = kwargs.get('meta')
261
+ if existing_meta:
262
+ meta_dict = existing_meta.model_dump() if hasattr(existing_meta, 'model_dump') else {}
263
+ meta_dict.update(_meta)
264
+ meta_obj = RequestParams.Meta(**meta_dict)
265
+ else:
266
+ meta_obj = RequestParams.Meta(**_meta)
267
+
268
+ # Create CallToolRequestParams without meta, then add _meta via model_dump
269
+ params = CallToolRequestParams(name=name, arguments=arguments)
270
+ params_dict = params.model_dump(by_alias=True)
271
+ params_dict["_meta"] = meta_obj.model_dump()
272
+
273
+ # Create request with proper types
274
+ request = CallToolRequest(
275
+ method="tools/call",
276
+ params=CallToolRequestParams.model_validate(params_dict)
277
+ )
278
+
279
+ return await self.send_request(request, CallToolResult)
280
+ else:
281
+ return await super().call_tool(name, arguments, **kwargs)
282
+
283
+ async def read_resource(self, uri: str, _meta: dict | None = None, **kwargs) -> ReadResourceResult:
284
+ """Read a resource with optional metadata support."""
285
+ if _meta:
286
+ from mcp.types import RequestParams
287
+
288
+ # Safe merge - preserve existing meta fields like progressToken
289
+ existing_meta = kwargs.get('meta')
290
+ if existing_meta:
291
+ meta_dict = existing_meta.model_dump() if hasattr(existing_meta, 'model_dump') else {}
292
+ meta_dict.update(_meta)
293
+ meta_obj = RequestParams.Meta(**meta_dict)
294
+ else:
295
+ meta_obj = RequestParams.Meta(**_meta)
296
+
297
+ request = ReadResourceRequest(
298
+ method="resources/read",
299
+ params=ReadResourceRequestParams(uri=uri, meta=meta_obj)
300
+ )
301
+ return await self.send_request(request, ReadResourceResult)
302
+ else:
303
+ return await super().read_resource(uri, **kwargs)
304
+
305
+ async def get_prompt(
306
+ self,
307
+ name: str,
308
+ arguments: dict | None = None,
309
+ _meta: dict | None = None,
310
+ **kwargs
311
+ ) -> GetPromptResult:
312
+ """Get a prompt with optional metadata support."""
313
+ if _meta:
314
+ from mcp.types import RequestParams
315
+
316
+ # Safe merge - preserve existing meta fields like progressToken
317
+ existing_meta = kwargs.get('meta')
318
+ if existing_meta:
319
+ meta_dict = existing_meta.model_dump() if hasattr(existing_meta, 'model_dump') else {}
320
+ meta_dict.update(_meta)
321
+ meta_obj = RequestParams.Meta(**meta_dict)
322
+ else:
323
+ meta_obj = RequestParams.Meta(**_meta)
324
+
325
+ request = GetPromptRequest(
326
+ method="prompts/get",
327
+ params=GetPromptRequestParams(name=name, arguments=arguments, meta=meta_obj)
328
+ )
329
+ return await self.send_request(request, GetPromptResult)
330
+ else:
331
+ return await super().get_prompt(name, arguments, **kwargs)
@@ -139,7 +139,10 @@ class MCPAggregator(ContextDependent):
139
139
 
140
140
  def _create_progress_callback(self, server_name: str, tool_name: str) -> "ProgressFnT":
141
141
  """Create a progress callback function for tool execution."""
142
- async def progress_callback(progress: float, total: float | None, message: str | None) -> None:
142
+
143
+ async def progress_callback(
144
+ progress: float, total: float | None, message: str | None
145
+ ) -> None:
143
146
  """Handle progress notifications from MCP tool execution."""
144
147
  logger.info(
145
148
  "Tool progress update",
@@ -153,6 +156,7 @@ class MCPAggregator(ContextDependent):
153
156
  "details": message or "", # Put the message in details column
154
157
  },
155
158
  )
159
+
156
160
  return progress_callback
157
161
 
158
162
  async def close(self) -> None:
@@ -508,12 +512,28 @@ class MCPAggregator(ContextDependent):
508
512
  async def try_execute(client: ClientSession):
509
513
  try:
510
514
  method = getattr(client, method_name)
515
+
516
+ # Get metadata from context for tool, resource, and prompt calls
517
+ metadata = None
518
+ if method_name in ["call_tool", "read_resource", "get_prompt"]:
519
+ from mcp_agent.llm.augmented_llm import _mcp_metadata_var
520
+
521
+ metadata = _mcp_metadata_var.get()
522
+
523
+ # Prepare kwargs
524
+ kwargs = method_args or {}
525
+ if metadata:
526
+ kwargs["_meta"] = metadata
527
+
511
528
  # For call_tool method, check if we need to add progress_callback
512
529
  if method_name == "call_tool" and progress_callback:
513
530
  # The call_tool method signature includes progress_callback parameter
514
- return await method(**method_args, progress_callback=progress_callback)
531
+ return await method(progress_callback=progress_callback, **kwargs)
515
532
  else:
516
- return await method(**method_args)
533
+ return await method(**(kwargs or {}))
534
+ except ConnectionError:
535
+ # Let ConnectionError pass through for reconnection logic
536
+ raise
517
537
  except Exception as e:
518
538
  error_msg = (
519
539
  f"Failed to {method_name} '{operation_name}' on server '{server_name}': {e}"
@@ -525,34 +545,77 @@ class MCPAggregator(ContextDependent):
525
545
  # Re-raise the original exception to propagate it
526
546
  raise e
527
547
 
528
- if self.connection_persistence:
529
- server_connection = await self._persistent_connection_manager.get_server(
530
- server_name, client_session_factory=MCPAgentClientSession
531
- )
532
- return await try_execute(server_connection.session)
533
- else:
534
- logger.debug(
535
- f"Creating temporary connection to server: {server_name}",
536
- data={
537
- "progress_action": ProgressAction.STARTING,
538
- "server_name": server_name,
539
- "agent_name": self.agent_name,
540
- },
541
- )
542
- async with gen_client(
543
- server_name, server_registry=self.context.server_registry
544
- ) as client:
545
- result = await try_execute(client)
548
+ # Try initial execution
549
+ try:
550
+ if self.connection_persistence:
551
+ server_connection = await self._persistent_connection_manager.get_server(
552
+ server_name, client_session_factory=MCPAgentClientSession
553
+ )
554
+ return await try_execute(server_connection.session)
555
+ else:
546
556
  logger.debug(
547
- f"Closing temporary connection to server: {server_name}",
557
+ f"Creating temporary connection to server: {server_name}",
548
558
  data={
549
- "progress_action": ProgressAction.SHUTDOWN,
559
+ "progress_action": ProgressAction.STARTING,
550
560
  "server_name": server_name,
551
561
  "agent_name": self.agent_name,
552
562
  },
553
563
  )
564
+ async with gen_client(
565
+ server_name, server_registry=self.context.server_registry
566
+ ) as client:
567
+ result = await try_execute(client)
568
+ logger.debug(
569
+ f"Closing temporary connection to server: {server_name}",
570
+ data={
571
+ "progress_action": ProgressAction.SHUTDOWN,
572
+ "server_name": server_name,
573
+ "agent_name": self.agent_name,
574
+ },
575
+ )
576
+ return result
577
+ except ConnectionError:
578
+ # Server offline - attempt reconnection
579
+ from mcp_agent import console
580
+
581
+ console.console.print(
582
+ f"[dim yellow]MCP server {server_name} reconnecting...[/dim yellow]"
583
+ )
584
+
585
+ try:
586
+ if self.connection_persistence:
587
+ # Force disconnect and create fresh connection
588
+ await self._persistent_connection_manager.disconnect_server(server_name)
589
+ import asyncio
590
+
591
+ await asyncio.sleep(0.1)
592
+
593
+ server_connection = await self._persistent_connection_manager.get_server(
594
+ server_name, client_session_factory=MCPAgentClientSession
595
+ )
596
+ result = await try_execute(server_connection.session)
597
+ else:
598
+ # For non-persistent connections, just try again
599
+ async with gen_client(
600
+ server_name, server_registry=self.context.server_registry
601
+ ) as client:
602
+ result = await try_execute(client)
603
+
604
+ # Success!
605
+ console.console.print(f"[dim green]MCP server {server_name} online[/dim green]")
554
606
  return result
555
607
 
608
+ except Exception:
609
+ # Reconnection failed
610
+ console.console.print(
611
+ f"[dim red]MCP server {server_name} offline - failed to reconnect[/dim red]"
612
+ )
613
+ error_msg = f"MCP server {server_name} offline - failed to reconnect"
614
+ if error_factory:
615
+ return error_factory(error_msg)
616
+ else:
617
+ raise Exception(error_msg)
618
+
556
619
  async def _parse_resource_name(self, name: str, resource_type: str) -> tuple[str, str]:
557
620
  """
558
621
  Parse a possibly namespaced resource name into server name and local resource name.
@@ -572,14 +635,13 @@ class MCPAggregator(ContextDependent):
572
635
 
573
636
  # Next, attempt to interpret as a namespaced name
574
637
  if is_namespaced_name(name):
575
- parts = name.split(SEP, 1)
576
- server_name, local_name = parts[0], parts[1]
638
+ # Try to match against known server names, handling server names with hyphens
639
+ for server_name in self.server_names:
640
+ if name.startswith(f"{server_name}{SEP}"):
641
+ local_name = name[len(server_name) + len(SEP) :]
642
+ return server_name, local_name
577
643
 
578
- # Validate that the parsed server actually exists
579
- if server_name in self.server_names:
580
- return server_name, local_name
581
-
582
- # If the server name doesn't exist, it might be a tool with a hyphen in its name
644
+ # If no server name matched, it might be a tool with a hyphen in its name
583
645
  # Fall through to the next checks
584
646
 
585
647
  # For tools, search all servers for the tool by exact name match
@@ -623,10 +685,10 @@ class MCPAggregator(ContextDependent):
623
685
  with tracer.start_as_current_span(f"MCP Tool: {server_name}/{local_tool_name}"):
624
686
  trace.get_current_span().set_attribute("tool_name", local_tool_name)
625
687
  trace.get_current_span().set_attribute("server_name", server_name)
626
-
688
+
627
689
  # Create progress callback for this tool execution
628
690
  progress_callback = self._create_progress_callback(server_name, local_tool_name)
629
-
691
+
630
692
  return await self._execute_on_server(
631
693
  server_name=server_name,
632
694
  operation_type="tool",
@@ -1236,11 +1298,11 @@ class MCPAggregator(ContextDependent):
1236
1298
  async def list_mcp_tools(self, server_name: str | None = None) -> Dict[str, List[Tool]]:
1237
1299
  """
1238
1300
  List available tools from one or all servers, grouped by server name.
1239
-
1301
+
1240
1302
  Args:
1241
1303
  server_name: Optional server name to list tools from. If not provided,
1242
1304
  lists tools from all servers.
1243
-
1305
+
1244
1306
  Returns:
1245
1307
  Dictionary mapping server names to lists of Tool objects (with original names, not namespaced)
1246
1308
  """
@@ -1249,7 +1311,7 @@ class MCPAggregator(ContextDependent):
1249
1311
 
1250
1312
  results: Dict[str, List[Tool]] = {}
1251
1313
 
1252
- # Get the list of servers to check
1314
+ # Get the list of servers to check
1253
1315
  servers_to_check = [server_name] if server_name else self.server_names
1254
1316
 
1255
1317
  # For each server, try to list its tools
@@ -272,6 +272,7 @@ class MCPConnectionManager(ContextDependent):
272
272
  # Manage our own task group - independent of task context
273
273
  self._task_group = None
274
274
  self._task_group_active = False
275
+ self._mcp_sse_filter_added = False
275
276
 
276
277
  async def __aenter__(self):
277
278
  # Create a task group that isn't tied to a specific task
@@ -300,6 +301,21 @@ class MCPConnectionManager(ContextDependent):
300
301
  except Exception as e:
301
302
  logger.error(f"Error during connection manager shutdown: {e}")
302
303
 
304
+ def _suppress_mcp_sse_errors(self) -> None:
305
+ """Suppress MCP library's 'Error in sse_reader' messages."""
306
+ if self._mcp_sse_filter_added:
307
+ return
308
+
309
+ import logging
310
+
311
+ class MCPSSEErrorFilter(logging.Filter):
312
+ def filter(self, record):
313
+ return not (record.name == "mcp.client.sse" and "Error in sse_reader" in record.getMessage())
314
+
315
+ mcp_sse_logger = logging.getLogger("mcp.client.sse")
316
+ mcp_sse_logger.addFilter(MCPSSEErrorFilter())
317
+ self._mcp_sse_filter_added = True
318
+
303
319
  async def launch_server(
304
320
  self,
305
321
  server_name: str,
@@ -341,6 +357,9 @@ class MCPConnectionManager(ContextDependent):
341
357
  logger.debug(f"{server_name}: Creating stdio client with custom error handler")
342
358
  return _add_none_to_context(stdio_client(server_params, errlog=error_handler))
343
359
  elif config.transport == "sse":
360
+ # Suppress MCP library error spam
361
+ self._suppress_mcp_sse_errors()
362
+
344
363
  return _add_none_to_context(
345
364
  sse_client(
346
365
  config.url,
@@ -19,6 +19,61 @@ from mcp_agent.mcp.mcp_aggregator import MCPAggregator
19
19
  HUMAN_INPUT_TOOL_NAME = "__human_input__"
20
20
  CODE_STYLE = "native"
21
21
 
22
+ HTML_ESCAPE_CHARS = {"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"}
23
+
24
+
25
+ def _prepare_markdown_content(content: str, escape_xml: bool = True) -> str:
26
+ """Prepare content for markdown rendering by escaping HTML/XML tags
27
+ while preserving code blocks and inline code.
28
+
29
+ This ensures XML/HTML tags are displayed as visible text rather than
30
+ being interpreted as markup by the markdown renderer.
31
+
32
+ Note: This method does not handle overlapping code blocks (e.g., if inline
33
+ code appears within a fenced code block range). In practice, this is not
34
+ an issue since markdown syntax doesn't support such overlapping.
35
+ """
36
+ if not escape_xml or not isinstance(content, str):
37
+ return content
38
+
39
+ protected_ranges = []
40
+ import re
41
+
42
+ # Protect fenced code blocks (don't escape anything inside these)
43
+ code_block_pattern = r"```[\s\S]*?```"
44
+ for match in re.finditer(code_block_pattern, content):
45
+ protected_ranges.append((match.start(), match.end()))
46
+
47
+ # Protect inline code (don't escape anything inside these)
48
+ inline_code_pattern = r"(?<!`)`(?!``)[^`\n]+`(?!`)"
49
+ for match in re.finditer(inline_code_pattern, content):
50
+ protected_ranges.append((match.start(), match.end()))
51
+
52
+ protected_ranges.sort(key=lambda x: x[0])
53
+
54
+ # Build the escaped content
55
+ result = []
56
+ last_end = 0
57
+
58
+ for start, end in protected_ranges:
59
+ # Escape everything outside protected ranges
60
+ unprotected_text = content[last_end:start]
61
+ for char, replacement in HTML_ESCAPE_CHARS.items():
62
+ unprotected_text = unprotected_text.replace(char, replacement)
63
+ result.append(unprotected_text)
64
+
65
+ # Keep protected ranges (code blocks) as-is
66
+ result.append(content[start:end])
67
+ last_end = end
68
+
69
+ # Escape any remaining content after the last protected range
70
+ remainder_text = content[last_end:]
71
+ for char, replacement in HTML_ESCAPE_CHARS.items():
72
+ remainder_text = remainder_text.replace(char, replacement)
73
+ result.append(remainder_text)
74
+
75
+ return "".join(result)
76
+
22
77
 
23
78
  class ConsoleDisplay:
24
79
  """
@@ -35,6 +90,49 @@ class ConsoleDisplay:
35
90
  """
36
91
  self.config = config
37
92
  self._markup = config.logger.enable_markup if config else True
93
+ self._escape_xml = True
94
+
95
+ def _render_content_smartly(self, content: str, check_markdown_markers: bool = False) -> None:
96
+ """
97
+ Helper method to intelligently render content based on its type.
98
+
99
+ - Pure XML: Use syntax highlighting for readability
100
+ - Markdown (with markers): Use markdown rendering with proper escaping
101
+ - Plain text: Display as-is (when check_markdown_markers=True and no markers found)
102
+
103
+ Args:
104
+ content: The text content to render
105
+ check_markdown_markers: If True, only use markdown rendering when markers are present
106
+ """
107
+ import re
108
+
109
+ from rich.markdown import Markdown
110
+
111
+ # Check if content appears to be primarily XML
112
+ xml_pattern = r"^<[a-zA-Z_][a-zA-Z0-9_-]*[^>]*>"
113
+ is_xml_content = bool(re.match(xml_pattern, content.strip())) and content.count("<") > 5
114
+
115
+ if is_xml_content:
116
+ # Display XML content with syntax highlighting for better readability
117
+ from rich.syntax import Syntax
118
+
119
+ syntax = Syntax(content, "xml", theme=CODE_STYLE, line_numbers=False)
120
+ console.console.print(syntax, markup=self._markup)
121
+ elif check_markdown_markers:
122
+ # Check for markdown markers before deciding to use markdown rendering
123
+ if any(marker in content for marker in ["##", "**", "*", "`", "---", "###"]):
124
+ # Has markdown markers - render as markdown with escaping
125
+ prepared_content = _prepare_markdown_content(content, self._escape_xml)
126
+ md = Markdown(prepared_content, code_theme=CODE_STYLE)
127
+ console.console.print(md, markup=self._markup)
128
+ else:
129
+ # Plain text - display as-is
130
+ console.console.print(content, markup=self._markup)
131
+ else:
132
+ # Always treat as markdown with proper escaping
133
+ prepared_content = _prepare_markdown_content(content, self._escape_xml)
134
+ md = Markdown(prepared_content, code_theme=CODE_STYLE)
135
+ console.console.print(md, markup=self._markup)
38
136
 
39
137
  def show_tool_result(self, result: CallToolResult, name: str | None = None) -> None:
40
138
  """Display a tool result in the new visual style."""
@@ -341,7 +439,6 @@ class ConsoleDisplay:
341
439
  model: str | None = None,
342
440
  ) -> None:
343
441
  """Display an assistant message in a formatted panel."""
344
- from rich.markdown import Markdown
345
442
 
346
443
  if not self.config or not self.config.logger.show_chat:
347
444
  return
@@ -385,9 +482,8 @@ class ConsoleDisplay:
385
482
  json = JSON(message_text)
386
483
  console.console.print(json, markup=self._markup)
387
484
  except (JSONDecodeError, TypeError, ValueError):
388
- # Not JSON, treat as markdown
389
- md = Markdown(content, code_theme=CODE_STYLE)
390
- console.console.print(md, markup=self._markup)
485
+ # Use the smart rendering helper to handle XML vs Markdown
486
+ self._render_content_smartly(content)
391
487
  else:
392
488
  # Handle Rich Text objects directly
393
489
  console.console.print(message_text, markup=self._markup)
@@ -475,7 +571,6 @@ class ConsoleDisplay:
475
571
  self, message, model: str | None = None, chat_turn: int = 0, name: str | None = None
476
572
  ) -> None:
477
573
  """Display a user message in the new visual style."""
478
- from rich.markdown import Markdown
479
574
 
480
575
  if not self.config or not self.config.logger.show_chat:
481
576
  return
@@ -495,9 +590,8 @@ class ConsoleDisplay:
495
590
 
496
591
  # Display content as markdown if it looks like markdown, otherwise as text
497
592
  if isinstance(message, str):
498
- content = message
499
- md = Markdown(content, code_theme=CODE_STYLE)
500
- console.console.print(md, markup=self._markup)
593
+ # Use the smart rendering helper to handle XML vs Markdown
594
+ self._render_content_smartly(message)
501
595
  else:
502
596
  # Handle Text objects directly
503
597
  console.console.print(message, markup=self._markup)
@@ -587,7 +681,7 @@ class ConsoleDisplay:
587
681
  Args:
588
682
  parallel_agent: The parallel agent containing fan_out_agents with results
589
683
  """
590
- from rich.markdown import Markdown
684
+
591
685
  from rich.text import Text
592
686
 
593
687
  if self.config and not self.config.logger.show_chat:
@@ -674,13 +768,9 @@ class ConsoleDisplay:
674
768
  console.console.print(left + " " * padding + right, markup=self._markup)
675
769
  console.console.print()
676
770
 
677
- # Display content as markdown if it looks like markdown, otherwise as text
771
+ # Display content based on its type (check for markdown markers in parallel results)
678
772
  content = result["content"]
679
- if any(marker in content for marker in ["##", "**", "*", "`", "---", "###"]):
680
- md = Markdown(content, code_theme=CODE_STYLE)
681
- console.console.print(md, markup=self._markup)
682
- else:
683
- console.console.print(content, markup=self._markup)
773
+ self._render_content_smartly(content, check_markdown_markers=True)
684
774
 
685
775
  # Summary
686
776
  console.console.print()