fast-agent-mcp 0.2.22__py3-none-any.whl → 0.2.24__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.
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fast-agent-mcp
3
- Version: 0.2.22
3
+ Version: 0.2.24
4
4
  Summary: Define, Prompt and Test MCP enabled Agents and Workflows
5
- Author-email: Shaun Smith <fastagent@llmindset.co.uk>, Sarmad Qadri <sarmad@lastmileai.dev>
5
+ Author-email: Shaun Smith <fastagent@llmindset.co.uk>
6
6
  License: Apache License
7
7
  Version 2.0, January 2004
8
8
  http://www.apache.org/licenses/
@@ -218,6 +218,7 @@ Requires-Dist: openai>=1.63.2
218
218
  Requires-Dist: opentelemetry-distro>=0.50b0
219
219
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.29.0
220
220
  Requires-Dist: opentelemetry-instrumentation-anthropic>=0.39.3
221
+ Requires-Dist: opentelemetry-instrumentation-mcp>=0.40.3
221
222
  Requires-Dist: opentelemetry-instrumentation-openai>=0.39.3
222
223
  Requires-Dist: prompt-toolkit>=3.0.50
223
224
  Requires-Dist: pydantic-settings>=2.7.0
@@ -1,11 +1,11 @@
1
1
  mcp_agent/__init__.py,sha256=18T0AG0W9sJhTY38O9GFFOzliDhxx9p87CvRyti9zbw,1620
2
2
  mcp_agent/app.py,sha256=WRsiUdwy_9IAnaGRDwuLm7pzgQpt2wgsg10vBOpfcwM,5539
3
- mcp_agent/config.py,sha256=ZC4SiIVbxVn7-hUfv3RFj6fNrXxvci6gmUNCGM7vzs8,12624
3
+ mcp_agent/config.py,sha256=L_wUWTdqFXaRTBA5tL_j2l_9dufWE_MHHPut5e89lBk,12773
4
4
  mcp_agent/console.py,sha256=Gjf2QLFumwG1Lav__c07X_kZxxEUSkzV-1_-YbAwcwo,813
5
- mcp_agent/context.py,sha256=Kb3s_0MolHx7AeTs1NVcY3ly-xFBd35o8LT7Srpx9is,7334
5
+ mcp_agent/context.py,sha256=5pnw78LgezCLeO5Os5dgmLDadwXqw_B4Ojib48XP1s4,7431
6
6
  mcp_agent/context_dependent.py,sha256=QXfhw3RaQCKfscEEBRGuZ3sdMWqkgShz2jJ1ivGGX1I,1455
7
7
  mcp_agent/event_progress.py,sha256=b1VKlQQF2AgPMb6XHjlJAVoPdx8GuxRTUk2g-4lBNm0,2749
8
- mcp_agent/mcp_server_registry.py,sha256=jUmCdfcpTitXm1-3TxpWsdRWY_8phdKNYgXwB16ZSVU,10100
8
+ mcp_agent/mcp_server_registry.py,sha256=QTzu0elBWzqXks6u5nI5n8uN5CX8CpyV6ybxnyt5LZM,11531
9
9
  mcp_agent/progress_display.py,sha256=GeJU9VUt6qKsFVymG688hCMVCsAygG9ifiiEb5IcbN4,361
10
10
  mcp_agent/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  mcp_agent/agents/agent.py,sha256=GgaUHoilgqzh9PQYr5k2WiPj4pagwicf9-ZLFsHkNNo,3848
@@ -13,7 +13,7 @@ mcp_agent/agents/base_agent.py,sha256=fjDr01-hZ9sB3ghI4DlXYVePP0s5f9pmtLH-N3X8bR
13
13
  mcp_agent/agents/workflow/__init__.py,sha256=HloteEW6kalvgR0XewpiFAqaQlMPlPJYg5p3K33IUzI,25
14
14
  mcp_agent/agents/workflow/chain_agent.py,sha256=eIlImirrSXkqBJmPuAJgOKis81Cl6lZEGM0-6IyaUV8,6105
15
15
  mcp_agent/agents/workflow/evaluator_optimizer.py,sha256=ysUMGM2NzeCIutgr_vXH6kUPpZMw0cX4J_Wl1r8eT84,13296
16
- mcp_agent/agents/workflow/orchestrator_agent.py,sha256=byZe4bx7D_7BSZZ3hN8BNUWVFPYeqeUwDUCLTRC8mlI,21583
16
+ mcp_agent/agents/workflow/orchestrator_agent.py,sha256=lArV7wHwPYepSuxe0ybTGJRJv85iebRI4ZOY_m8kMZQ,21593
17
17
  mcp_agent/agents/workflow/orchestrator_models.py,sha256=5P_aXADVT4Et8qT4e1cb9RelmHX5dCRrzu8j8T41Kdg,7230
18
18
  mcp_agent/agents/workflow/orchestrator_prompts.py,sha256=EXKEI174sshkZyPPEnWbwwNafzSPuA39MXL7iqG9cWc,9106
19
19
  mcp_agent/agents/workflow/parallel_agent.py,sha256=JaQFp35nmAdoBRLAwx8BfnK7kirVq9PMw24LQ3ZEzoc,7705
@@ -23,9 +23,10 @@ mcp_agent/cli/__main__.py,sha256=AVZ7tQFhU_sDOGuUGJq8ujgKtcxsYJBJwHbVaaiRDlI,166
23
23
  mcp_agent/cli/main.py,sha256=XjrgXMBaPKkVqAFo8T9LJz6Tp1-ivrKDOuNYWke99YA,3090
24
24
  mcp_agent/cli/terminal.py,sha256=GRwD-RGW7saIz2IOWZn5vD6JjiArscELBThm1GTFkuI,1065
25
25
  mcp_agent/cli/commands/check_config.py,sha256=9Ryxo_fLInm3YKdYv46yLrAJgnQtMisGreu6Kkriw2g,16677
26
- mcp_agent/cli/commands/go.py,sha256=2UY8TSDwhh_-p-WYXrZz3pEv3-2eTdBl5Lxy3JyJV0E,6057
26
+ mcp_agent/cli/commands/go.py,sha256=LIsOJQuTdfCUcNm7JT-NQDU8cI-GCnYwYjN2VOWxvqs,8658
27
27
  mcp_agent/cli/commands/quickstart.py,sha256=SM3CHMzDgvTxIpKjFuX9BrS_N1vRoXNBDaO90aWx1Rk,14586
28
28
  mcp_agent/cli/commands/setup.py,sha256=eOEd4TL-b0DaDeSJMGOfNOsTEItoZ67W88eTP4aP-bo,6482
29
+ mcp_agent/cli/commands/url_parser.py,sha256=7QL9bp9tO7w0cPnwhbpt8GwjbOJ1Rrry1o71uVJhSss,5655
29
30
  mcp_agent/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
31
  mcp_agent/core/agent_app.py,sha256=5nQJNo8DocIRWiX4pVKAHUZF8s6HWpc-hJnfzl_1v1c,9697
31
32
  mcp_agent/core/agent_types.py,sha256=bQVQMTwKH7qHIJsNglj4C_d6PNFBBzC_0RIkcENSII4,1459
@@ -34,11 +35,11 @@ mcp_agent/core/direct_factory.py,sha256=d96OM1yS3eIocIiaA9FQt6C2zr6VDUyCJBTZCp_D
34
35
  mcp_agent/core/enhanced_prompt.py,sha256=bzvcengS7XzHWB7NWhyxHM3hhO2HI4zP5DbGXAOw0Jw,19155
35
36
  mcp_agent/core/error_handling.py,sha256=xoyS2kLe0eG0bj2eSJCJ2odIhGUve2SbDR7jP-A-uRw,624
36
37
  mcp_agent/core/exceptions.py,sha256=ENAD_qGG67foxy6vDkIvc-lgopIUQy6O7zvNPpPXaQg,2289
37
- mcp_agent/core/fastagent.py,sha256=WEEGz2WBAddDGNeWJwqwFIPLiQnLjaNxZLoMR0peyyU,22884
38
+ mcp_agent/core/fastagent.py,sha256=uS_NSXeniUYFu6xce8OHGJ9PbEYNU-gm1XVpa1r0rZc,22893
38
39
  mcp_agent/core/interactive_prompt.py,sha256=w3VyRzW4hzn0xhWZRwo_qRRAD5WVSrJYe8QDe1XZ55Y,24252
39
40
  mcp_agent/core/mcp_content.py,sha256=2D7KHY9mG_vxoDwFLKvsPQV9VRIzHItM7V-jcEnACh8,8878
40
41
  mcp_agent/core/prompt.py,sha256=qnintOUGEoDPYLI9bu9G2OlgVMCe5ZPUZilgMzydXhc,7919
41
- mcp_agent/core/request_params.py,sha256=vRfAz9T6Ir-0oeJ4qEdO62LDOzoLwBuuXcBcdh6WPZ8,1576
42
+ mcp_agent/core/request_params.py,sha256=qmFWZXeYEJyYw2IwonyrTnZWxQG7qX6bKpOPcqETa60,1603
42
43
  mcp_agent/core/validation.py,sha256=RIBKFlh0GJg4rTcFQXoXp8A0sK1HpsCigKcYSK3gFaY,12090
43
44
  mcp_agent/executor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
45
  mcp_agent/executor/executor.py,sha256=E44p6d-o3OMRoP_dNs_cDnyti91LQ3P9eNU88mSi1kc,9462
@@ -64,7 +65,7 @@ mcp_agent/llm/providers/augmented_llm_anthropic.py,sha256=gK_IvllVBNJUUrSfpgFpdh
64
65
  mcp_agent/llm/providers/augmented_llm_deepseek.py,sha256=NiZK5nv91ZS2VgVFXpbsFNFYLsLcppcbo_RstlRMd7I,1145
65
66
  mcp_agent/llm/providers/augmented_llm_generic.py,sha256=5Uq8ZBhcFuQTt7koP_5ykolREh2iWu8zKhNbh3pM9lQ,1210
66
67
  mcp_agent/llm/providers/augmented_llm_google.py,sha256=N0a2fphVtkvNYxKQpEX6J4tlO1C_mRw4sw3LBXnrOeI,1130
67
- mcp_agent/llm/providers/augmented_llm_openai.py,sha256=0C7BOB7i3xo0HsMCTagRSQ8Hsywb-31mot26OfohzCU,14478
68
+ mcp_agent/llm/providers/augmented_llm_openai.py,sha256=jbLG9t0iuneRPX0Cscim6K48SJEB5vPopDE3IBmJ708,14515
68
69
  mcp_agent/llm/providers/augmented_llm_openrouter.py,sha256=V_TlVKm92GHBxYIo6gpvH_6cAaIdppS25Tz6x5T7LW0,2341
69
70
  mcp_agent/llm/providers/augmented_llm_tensorzero.py,sha256=Mol_Wzj_ZtccW-LMw0oFwWUt1m1yfofloay9QYNP23c,20729
70
71
  mcp_agent/llm/providers/multipart_converter_anthropic.py,sha256=t5lHYGfFUacJldnrVtMNW-8gEMoto8Y7hJkDrnyZR-Y,16650
@@ -85,9 +86,9 @@ mcp_agent/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
86
  mcp_agent/mcp/gen_client.py,sha256=fAVwFVCgSamw4PwoWOV4wrK9TABx1S_zZv8BctRyF2k,3030
86
87
  mcp_agent/mcp/interfaces.py,sha256=PAou8znAl2HgtvfCpLQOZFbKra9F72OcVRfBJbboNX8,6965
87
88
  mcp_agent/mcp/logger_textio.py,sha256=vljC1BtNTCxBAda9ExqNB-FwVNUZIuJT3h1nWmCjMws,3172
88
- mcp_agent/mcp/mcp_agent_client_session.py,sha256=Ng7epBXq8BEA_3m1GX5LqwafgNUAMSzBugwN6N0VUWQ,4364
89
- mcp_agent/mcp/mcp_aggregator.py,sha256=lVSt0yp0CnaYjcHCWmluwBeFgl8JXHYEZk0MzXgrQzA,40110
90
- mcp_agent/mcp/mcp_connection_manager.py,sha256=6jtjclh4YNJZsNwYnSWmQ6cPzapAwsRUxir1c_gVNfM,16051
89
+ mcp_agent/mcp/mcp_agent_client_session.py,sha256=4597ww1ihSKh-zKc9xMF3ODqosVPU_A4xVmUbk1DvcE,6002
90
+ mcp_agent/mcp/mcp_aggregator.py,sha256=_zqSuWGwRTLleXldQjqPSNqV0RRRr1luJIZOvB2AdRg,46011
91
+ mcp_agent/mcp/mcp_connection_manager.py,sha256=jlqaAdS4zc1UfVBHQU0TkTbVr0-rOkbN9bkrLPrZVLk,17159
91
92
  mcp_agent/mcp/mime_utils.py,sha256=difepNR_gpb4MpMLkBRAoyhDk-AjXUHTiqKvT_VwS1o,1805
92
93
  mcp_agent/mcp/prompt_message_multipart.py,sha256=BDwRdNwyWHb2q2bccDb2iR2VlORqVvkvoG3xYzcMpCE,4403
93
94
  mcp_agent/mcp/prompt_render.py,sha256=k3v4BZDThGE2gGiOYVQtA6x8WTEdOuXIEnRafANhN1U,2996
@@ -101,10 +102,10 @@ mcp_agent/mcp/prompts/__main__.py,sha256=gr1Tdz9fcK0EXjEuZg_BOnKUmvhYq5AH2lFZicV
101
102
  mcp_agent/mcp/prompts/prompt_constants.py,sha256=Q9W0t3rOXl2LHIG9wcghApUV2QZ1iICuo7SwVwHUf3c,566
102
103
  mcp_agent/mcp/prompts/prompt_helpers.py,sha256=Joqo2t09pTKDP-Wge3G-ozPEHikzjaqwV6GVk8hNR50,7534
103
104
  mcp_agent/mcp/prompts/prompt_load.py,sha256=Zo0FogqWFEG5FtF1d9ZH-RWsCSSMsi5FIEQHpJD8N7M,5404
104
- mcp_agent/mcp/prompts/prompt_server.py,sha256=DbuDcYCMbsbqwBeebpNGInAQ4-DP1Jjp49y8uZ-0XlY,18872
105
+ mcp_agent/mcp/prompts/prompt_server.py,sha256=VAKS4rHTE5Mp7e0NV6qADslR_5vSLab8RUhNxCkAdJE,19234
105
106
  mcp_agent/mcp/prompts/prompt_template.py,sha256=EejiqGkau8OizORNyKTUwUjrPof5V-hH1H_MBQoQfXw,15732
106
107
  mcp_agent/mcp_server/__init__.py,sha256=zBU51ITHIEPScd9nRafnhEddsWqXRPAAvHhkrbRI2_4,155
107
- mcp_agent/mcp_server/agent_server.py,sha256=s-nI0uTNWx4nYDDM_5GmuY5x6ZeFkymfNoCSuwuBRd8,19891
108
+ mcp_agent/mcp_server/agent_server.py,sha256=df3UbPLg52e_SS98F3lc4T8BqqzvQRBl6kplODsaq-M,20096
108
109
  mcp_agent/resources/examples/data-analysis/analysis-campaign.py,sha256=16gxrQ5kM8fb8tPwSCMXaitonk3PSEhz28njWwPxXrw,7269
109
110
  mcp_agent/resources/examples/data-analysis/analysis.py,sha256=M9z8Q4YC5OGuqSa5uefYmmfmctqMn-WqCSfg5LI407o,2609
110
111
  mcp_agent/resources/examples/data-analysis/fastagent.config.yaml,sha256=ini94PHyJCfgpjcjHKMMbGuHs6LIj46F1NwY0ll5HVk,1609
@@ -123,7 +124,7 @@ mcp_agent/resources/examples/internal/sizer.py,sha256=xP1TBJkp4xIdtJnyk2MP4BufTh
123
124
  mcp_agent/resources/examples/internal/social.py,sha256=pTKcpHAcvA-vQYgjVfDuU1FivCR004Nq4N2GXd5OMs0,1716
124
125
  mcp_agent/resources/examples/mcp/state-transfer/agent_one.py,sha256=HR-Igr8k68HU0tqIpaXujtJxnKSUwwtZqTdZk8QHNgo,455
125
126
  mcp_agent/resources/examples/mcp/state-transfer/agent_two.py,sha256=TY9SPzJZFv3TL6VEP3IpdJvTjYup5txF_DjpvEzlmbw,476
126
- mcp_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml,sha256=-IIocaehANnWiwFHSNNzDRdV80ApicNmxAY4flKETpk,797
127
+ mcp_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml,sha256=e3Esqw850p9GcapVVhPhAAHWwtt2gH2ivsVIULD9n6Q,798
127
128
  mcp_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example,sha256=0n3F2S_Z2CeLHZueZqCGy37mxiMDHLplvxUHYiCpD2A,421
128
129
  mcp_agent/resources/examples/prompting/__init__.py,sha256=2GSrs9MSDIKo-uDrUI0O311F0UH0RW02ZNdvItJzjfI,50
129
130
  mcp_agent/resources/examples/prompting/agent.py,sha256=HxzUsidfxoc7Th0Ws55ppQCHNLkdZvcbiAcc2fMd4KI,490
@@ -144,9 +145,9 @@ mcp_agent/resources/examples/workflows/orchestrator.py,sha256=rOGilFTliWWnZ3Jx5w
144
145
  mcp_agent/resources/examples/workflows/parallel.py,sha256=DQ5vY5-h8Qa5QHcYjsWXhZ_FYrYoloVWOdgeXV9p2gI,1890
145
146
  mcp_agent/resources/examples/workflows/router.py,sha256=E4x_-c3l4YW9w1i4ARcDtkdeqIdbWEGfsMzwLYpdbVc,1677
146
147
  mcp_agent/resources/examples/workflows/short_story.txt,sha256=X3y_1AyhLFN2AKzCKvucJtDgAFIJfnlbsbGZO5bBWu0,1187
147
- mcp_agent/ui/console_display.py,sha256=TVGDtJ37hc6UG0ei9g7ZPZZfFNeS1MYozt-Mx8HsPCk,9752
148
- fast_agent_mcp-0.2.22.dist-info/METADATA,sha256=HbiOG6NhC3IEfEyBsUDQnihh2LbU4CEZEe19X4Y7VTQ,30194
149
- fast_agent_mcp-0.2.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
150
- fast_agent_mcp-0.2.22.dist-info/entry_points.txt,sha256=bRniFM5zk3Kix5z7scX0gf9VnmGQ2Cz_Q1Gh7Ir4W00,186
151
- fast_agent_mcp-0.2.22.dist-info/licenses/LICENSE,sha256=cN3FxDURL9XuzE5mhK9L2paZo82LTfjwCYVT7e3j0e4,10939
152
- fast_agent_mcp-0.2.22.dist-info/RECORD,,
148
+ mcp_agent/ui/console_display.py,sha256=EUeMJ7yqtxJ0-hAjXNZFJFTlmfyKlENdfzTlw0e5ETg,9949
149
+ fast_agent_mcp-0.2.24.dist-info/METADATA,sha256=BxBQ5fsAWpeZ2UhH_zChQb816cZp6_ipY05ea60lKJA,30213
150
+ fast_agent_mcp-0.2.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
151
+ fast_agent_mcp-0.2.24.dist-info/entry_points.txt,sha256=bRniFM5zk3Kix5z7scX0gf9VnmGQ2Cz_Q1Gh7Ir4W00,186
152
+ fast_agent_mcp-0.2.24.dist-info/licenses/LICENSE,sha256=cN3FxDURL9XuzE5mhK9L2paZo82LTfjwCYVT7e3j0e4,10939
153
+ fast_agent_mcp-0.2.24.dist-info/RECORD,,
@@ -229,7 +229,7 @@ class OrchestratorAgent(BaseAgent):
229
229
  self.logger.warning(
230
230
  f"Reached maximum step limit ({max_steps}) without completing objective"
231
231
  )
232
- plan_result.max_steps_reached = True
232
+ plan_result.max_iterations_reached = True
233
233
  break
234
234
 
235
235
  # Execute the step and collect results
@@ -239,7 +239,7 @@ class OrchestratorAgent(BaseAgent):
239
239
  total_steps_executed += 1
240
240
 
241
241
  # Check if we need to break due to hitting max steps
242
- if getattr(plan_result, "max_steps_reached", False):
242
+ if getattr(plan_result, "max_iterations_reached", False):
243
243
  break
244
244
 
245
245
  # If the plan is marked complete, finalize the result
@@ -2,16 +2,18 @@
2
2
 
3
3
  import asyncio
4
4
  import sys
5
- from typing import List, Optional
5
+ from typing import Dict, List, Optional
6
6
 
7
7
  import typer
8
8
 
9
+ from mcp_agent.cli.commands.url_parser import generate_server_configs, parse_server_urls
9
10
  from mcp_agent.core.fastagent import FastAgent
10
11
 
11
12
  app = typer.Typer(
12
13
  help="Run an interactive agent directly from the command line without creating an agent.py file"
13
14
  )
14
15
 
16
+
15
17
  async def _run_agent(
16
18
  name: str = "FastAgent CLI",
17
19
  instruction: str = "You are a helpful AI Agent.",
@@ -19,33 +21,61 @@ async def _run_agent(
19
21
  server_list: Optional[List[str]] = None,
20
22
  model: Optional[str] = None,
21
23
  message: Optional[str] = None,
22
- prompt_file: Optional[str] = None
24
+ prompt_file: Optional[str] = None,
25
+ url_servers: Optional[Dict[str, Dict[str, str]]] = None,
23
26
  ) -> None:
24
27
  """Async implementation to run an interactive agent."""
25
28
  from pathlib import Path
26
29
 
30
+ from mcp_agent.config import MCPServerSettings, MCPSettings
27
31
  from mcp_agent.mcp.prompts.prompt_load import load_prompt_multipart
28
32
 
29
- # Create the FastAgent instance with CLI arg parsing enabled
30
- # It will automatically parse args like --model, --quiet, etc.
33
+ # Create the FastAgent instance
31
34
  fast_kwargs = {
32
35
  "name": name,
33
36
  "config_path": config_path,
34
37
  "ignore_unknown_args": True,
35
38
  "parse_cli_args": False, # Don't parse CLI args, we're handling it ourselves
36
39
  }
37
-
40
+
38
41
  fast = FastAgent(**fast_kwargs)
39
42
 
43
+ # Add URL-based servers to the context configuration
44
+ if url_servers:
45
+ # Initialize the app to ensure context is ready
46
+ await fast.app.initialize()
47
+
48
+ # Initialize mcp settings if needed
49
+ if not hasattr(fast.app.context.config, "mcp"):
50
+ fast.app.context.config.mcp = MCPSettings()
51
+
52
+ # Initialize servers dictionary if needed
53
+ if (
54
+ not hasattr(fast.app.context.config.mcp, "servers")
55
+ or fast.app.context.config.mcp.servers is None
56
+ ):
57
+ fast.app.context.config.mcp.servers = {}
58
+
59
+ # Add each URL server to the config
60
+ for server_name, server_config in url_servers.items():
61
+ server_settings = {"transport": server_config["transport"], "url": server_config["url"]}
62
+
63
+ # Add headers if present in the server config
64
+ if "headers" in server_config:
65
+ server_settings["headers"] = server_config["headers"]
66
+
67
+ fast.app.context.config.mcp.servers[server_name] = MCPServerSettings(**server_settings)
68
+
40
69
  # Define the agent with specified parameters
41
70
  agent_kwargs = {"instruction": instruction}
42
71
  if server_list:
43
72
  agent_kwargs["servers"] = server_list
44
73
  if model:
45
74
  agent_kwargs["model"] = model
46
-
75
+
47
76
  # Handle prompt file and message options
48
77
  if message or prompt_file:
78
+
49
79
  @fast.agent(**agent_kwargs)
50
80
  async def cli_agent():
51
81
  async with fast.run() as agent:
@@ -55,7 +85,7 @@ async def _run_agent(
55
85
  print(response)
56
86
  elif prompt_file:
57
87
  prompt = load_prompt_multipart(Path(prompt_file))
58
- response = await agent.generate(prompt)
88
+ response = await agent.default.generate(prompt)
59
89
  # Print the response text and exit
60
90
  print(response.last_text())
61
91
  else:
@@ -68,18 +98,37 @@ async def _run_agent(
68
98
  # Run the agent
69
99
  await cli_agent()
70
100
 
101
+
71
102
  def run_async_agent(
72
- name: str,
73
- instruction: str,
74
- config_path: Optional[str] = None,
103
+ name: str,
104
+ instruction: str,
105
+ config_path: Optional[str] = None,
75
106
  servers: Optional[str] = None,
107
+ urls: Optional[str] = None,
108
+ auth: Optional[str] = None,
76
109
  model: Optional[str] = None,
77
110
  message: Optional[str] = None,
78
- prompt_file: Optional[str] = None
111
+ prompt_file: Optional[str] = None,
79
112
  ):
80
113
  """Run the async agent function with proper loop handling."""
81
- server_list = servers.split(',') if servers else None
82
-
114
+ server_list = servers.split(",") if servers else None
115
+
116
+ # Parse URLs and generate server configurations if provided
117
+ url_servers = None
118
+ if urls:
119
+ try:
120
+ parsed_urls = parse_server_urls(urls, auth)
121
+ url_servers = generate_server_configs(parsed_urls)
122
+ # If we have servers from URLs, add their names to the server_list
123
+ if url_servers and not server_list:
124
+ server_list = list(url_servers.keys())
125
+ elif url_servers and server_list:
126
+ # Merge both lists
127
+ server_list.extend(list(url_servers.keys()))
128
+ except ValueError as e:
129
+ print(f"Error parsing URLs: {e}")
130
+ return
131
+
83
132
  # Check if we're already in an event loop
84
133
  try:
85
134
  loop = asyncio.get_event_loop()
@@ -92,24 +141,27 @@ def run_async_agent(
92
141
  # No event loop exists, so we'll create one
93
142
  loop = asyncio.new_event_loop()
94
143
  asyncio.set_event_loop(loop)
95
-
144
+
96
145
  try:
97
- loop.run_until_complete(_run_agent(
98
- name=name,
99
- instruction=instruction,
100
- config_path=config_path,
101
- server_list=server_list,
102
- model=model,
103
- message=message,
104
- prompt_file=prompt_file
105
- ))
146
+ loop.run_until_complete(
147
+ _run_agent(
148
+ name=name,
149
+ instruction=instruction,
150
+ config_path=config_path,
151
+ server_list=server_list,
152
+ model=model,
153
+ message=message,
154
+ prompt_file=prompt_file,
155
+ url_servers=url_servers,
156
+ )
157
+ )
106
158
  finally:
107
159
  try:
108
160
  # Clean up the loop
109
161
  tasks = asyncio.all_tasks(loop)
110
162
  for task in tasks:
111
163
  task.cancel()
112
-
164
+
113
165
  # Run the event loop until all tasks are done
114
166
  if sys.version_info >= (3, 7):
115
167
  loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
@@ -118,6 +170,7 @@ def run_async_agent(
118
170
  except Exception:
119
171
  pass
120
172
 
173
+
121
174
  @app.callback(invoke_without_command=True)
122
175
  def go(
123
176
  ctx: typer.Context,
@@ -131,6 +184,12 @@ def go(
131
184
  servers: Optional[str] = typer.Option(
132
185
  None, "--servers", help="Comma-separated list of server names to enable from config"
133
186
  ),
187
+ urls: Optional[str] = typer.Option(
188
+ None, "--url", help="Comma-separated list of HTTP/SSE URLs to connect to"
189
+ ),
190
+ auth: Optional[str] = typer.Option(
191
+ None, "--auth", help="Bearer token for authorization with URL-based servers"
192
+ ),
134
193
  model: Optional[str] = typer.Option(
135
194
  None, "--model", help="Override the default model (e.g., haiku, sonnet, gpt-4)"
136
195
  ),
@@ -148,6 +207,8 @@ def go(
148
207
  fast-agent go --model=haiku --instruction="You are a coding assistant" --servers=fetch,filesystem
149
208
  fast-agent go --message="What is the weather today?" --model=haiku
150
209
  fast-agent go --prompt-file=my-prompt.txt --model=haiku
210
+ fast-agent go --url=http://localhost:8001/mcp,http://api.example.com/sse
211
+ fast-agent go --url=https://api.example.com/mcp --auth=YOUR_API_TOKEN
151
212
 
152
213
  This will start an interactive session with the agent, using the specified model
153
214
  and instruction. It will use the default configuration from fastagent.config.yaml
@@ -157,15 +218,19 @@ def go(
157
218
  --model Override the default model (e.g., --model=haiku)
158
219
  --quiet Disable progress display and logging
159
220
  --servers Comma-separated list of server names to enable from config
221
+ --url Comma-separated list of HTTP/SSE URLs to connect to
222
+ --auth Bearer token for authorization with URL-based servers
160
223
  --message, -m Send a single message and exit
161
224
  --prompt-file, -p Use a prompt file instead of interactive mode
162
225
  """
163
226
  run_async_agent(
164
- name=name,
165
- instruction=instruction,
166
- config_path=config_path,
227
+ name=name,
228
+ instruction=instruction,
229
+ config_path=config_path,
167
230
  servers=servers,
231
+ urls=urls,
232
+ auth=auth,
168
233
  model=model,
169
234
  message=message,
170
- prompt_file=prompt_file
171
- )
235
+ prompt_file=prompt_file,
236
+ )
@@ -0,0 +1,185 @@
1
+ """
2
+ URL parsing utility for the fast-agent CLI.
3
+ Provides functions to parse URLs and determine MCP server configurations.
4
+ """
5
+
6
+ import hashlib
7
+ import re
8
+ from typing import Dict, List, Literal, Tuple
9
+ from urllib.parse import urlparse
10
+
11
+
12
+ def parse_server_url(
13
+ url: str,
14
+ ) -> Tuple[str, Literal["http", "sse"], str]:
15
+ """
16
+ Parse a server URL and determine the transport type and server name.
17
+
18
+ Args:
19
+ url: The URL to parse
20
+
21
+ Returns:
22
+ Tuple containing:
23
+ - server_name: A generated name for the server
24
+ - transport_type: Either "http" or "sse" based on URL
25
+ - url: The parsed and validated URL
26
+
27
+ Raises:
28
+ ValueError: If the URL is invalid or unsupported
29
+ """
30
+ # Basic URL validation
31
+ if not url:
32
+ raise ValueError("URL cannot be empty")
33
+
34
+ # Parse the URL
35
+ parsed_url = urlparse(url)
36
+
37
+ # Ensure scheme is present and is either http or https
38
+ if not parsed_url.scheme or parsed_url.scheme not in ("http", "https"):
39
+ raise ValueError(f"URL must have http or https scheme: {url}")
40
+
41
+ # Ensure netloc (hostname) is present
42
+ if not parsed_url.netloc:
43
+ raise ValueError(f"URL must include a hostname: {url}")
44
+
45
+ # Determine transport type based on URL path
46
+ transport_type: Literal["http", "sse"] = "http"
47
+ if parsed_url.path.endswith("/sse"):
48
+ transport_type = "sse"
49
+ elif not parsed_url.path.endswith("/mcp"):
50
+ # If path doesn't end with /mcp or /sse, append /mcp
51
+ url = url if url.endswith("/") else f"{url}/"
52
+ url = f"{url}mcp"
53
+
54
+ # Generate a server name based on hostname and port
55
+ server_name = generate_server_name(url)
56
+
57
+ return server_name, transport_type, url
58
+
59
+
60
+ def generate_server_name(url: str) -> str:
61
+ """
62
+ Generate a unique and readable server name from a URL.
63
+
64
+ Args:
65
+ url: The URL to generate a name for
66
+
67
+ Returns:
68
+ A server name string
69
+ """
70
+ parsed_url = urlparse(url)
71
+
72
+ # Extract hostname and port
73
+ hostname = parsed_url.netloc.split(":")[0]
74
+
75
+ # Clean the hostname for use in a server name
76
+ # Replace non-alphanumeric characters with underscores
77
+ clean_hostname = re.sub(r"[^a-zA-Z0-9]", "_", hostname)
78
+
79
+ if len(clean_hostname) > 15:
80
+ clean_hostname = clean_hostname[:9] + clean_hostname[-5:]
81
+
82
+ # If it's localhost or an IP, add a more unique identifier
83
+ if clean_hostname in ("localhost", "127_0_0_1") or re.match(r"^(\d+_){3}\d+$", clean_hostname):
84
+ # Use the path as part of the name for uniqueness
85
+ path = parsed_url.path.strip("/")
86
+ path = re.sub(r"[^a-zA-Z0-9]", "_", path)
87
+
88
+ # Include port if specified
89
+ port = ""
90
+ if ":" in parsed_url.netloc:
91
+ port = f"_{parsed_url.netloc.split(':')[1]}"
92
+
93
+ if path:
94
+ return f"{clean_hostname}{port}_{path[:20]}" # Limit path length
95
+ else:
96
+ # Use a hash if no path for uniqueness
97
+ url_hash = hashlib.md5(url.encode()).hexdigest()[:8]
98
+ return f"{clean_hostname}{port}_{url_hash}"
99
+
100
+ return clean_hostname
101
+
102
+
103
+ def parse_server_urls(
104
+ urls_param: str, auth_token: str = None
105
+ ) -> List[Tuple[str, Literal["http", "sse"], str, Dict[str, str] | None]]:
106
+ """
107
+ Parse a comma-separated list of URLs into server configurations.
108
+
109
+ Args:
110
+ urls_param: Comma-separated list of URLs
111
+ auth_token: Optional bearer token for authorization
112
+
113
+ Returns:
114
+ List of tuples containing (server_name, transport_type, url, headers)
115
+
116
+ Raises:
117
+ ValueError: If any URL is invalid
118
+ """
119
+ if not urls_param:
120
+ return []
121
+
122
+ # Split by comma and strip whitespace
123
+ url_list = [url.strip() for url in urls_param.split(",")]
124
+
125
+ # Prepare headers if auth token is provided
126
+ headers = None
127
+ if auth_token:
128
+ headers = {"Authorization": f"Bearer {auth_token}"}
129
+
130
+ # Parse each URL
131
+ result = []
132
+ for url in url_list:
133
+ server_name, transport_type, parsed_url = parse_server_url(url)
134
+ result.append((server_name, transport_type, parsed_url, headers))
135
+
136
+ return result
137
+
138
+
139
+ def generate_server_configs(
140
+ parsed_urls: List[Tuple[str, Literal["http", "sse"], str, Dict[str, str] | None]],
141
+ ) -> Dict[str, Dict[str, str | Dict[str, str]]]:
142
+ """
143
+ Generate server configurations from parsed URLs.
144
+
145
+ Args:
146
+ parsed_urls: List of tuples containing (server_name, transport_type, url, headers)
147
+
148
+ Returns:
149
+ Dictionary of server configurations
150
+ """
151
+ server_configs = {}
152
+ # Keep track of server name occurrences to handle collisions
153
+ name_counts = {}
154
+
155
+ for server_name, transport_type, url, headers in parsed_urls:
156
+ # Handle name collisions by adding a suffix
157
+ final_name = server_name
158
+ if server_name in server_configs:
159
+ # Initialize counter if we haven't seen this name yet
160
+ if server_name not in name_counts:
161
+ name_counts[server_name] = 1
162
+
163
+ # Generate a new name with suffix
164
+ suffix = name_counts[server_name]
165
+ final_name = f"{server_name}_{suffix}"
166
+ name_counts[server_name] += 1
167
+
168
+ # Ensure the new name is also unique
169
+ while final_name in server_configs:
170
+ suffix = name_counts[server_name]
171
+ final_name = f"{server_name}_{suffix}"
172
+ name_counts[server_name] += 1
173
+
174
+ config = {
175
+ "transport": transport_type,
176
+ "url": url,
177
+ }
178
+
179
+ # Add headers if provided
180
+ if headers:
181
+ config["headers"] = headers
182
+
183
+ server_configs[final_name] = config
184
+
185
+ return server_configs
mcp_agent/config.py CHANGED
@@ -60,7 +60,7 @@ class MCPServerSettings(BaseModel):
60
60
  description: str | None = None
61
61
  """The description of the server."""
62
62
 
63
- transport: Literal["stdio", "sse"] = "stdio"
63
+ transport: Literal["stdio", "sse", "http"] = "stdio"
64
64
  """The transport mechanism."""
65
65
 
66
66
  command: str | None = None
@@ -249,6 +249,8 @@ class LoggerSettings(BaseModel):
249
249
  """Show MCP Sever tool calls on the console"""
250
250
  truncate_tools: bool = True
251
251
  """Truncate display of long tool calls"""
252
+ enable_markup: bool = True
253
+ """Enable markup in console output. Disable for outputs that may conflict with rich console formatting"""
252
254
 
253
255
 
254
256
  class Settings(BaseSettings):
mcp_agent/context.py CHANGED
@@ -11,6 +11,7 @@ from mcp import ServerSession
11
11
  from opentelemetry import trace
12
12
  from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
13
13
  from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
14
+ from opentelemetry.instrumentation.mcp import McpInstrumentor
14
15
  from opentelemetry.instrumentation.openai import OpenAIInstrumentor
15
16
  from opentelemetry.propagate import set_global_textmap
16
17
  from opentelemetry.sdk.resources import Resource
@@ -111,6 +112,7 @@ async def configure_otel(config: "Settings") -> None:
111
112
  trace.set_tracer_provider(tracer_provider)
112
113
  AnthropicInstrumentor().instrument()
113
114
  OpenAIInstrumentor().instrument()
115
+ McpInstrumentor().instrument()
114
116
 
115
117
 
116
118
  async def configure_logger(config: "Settings") -> None:
@@ -131,8 +131,8 @@ class FastAgent:
131
131
  )
132
132
  parser.add_argument(
133
133
  "--transport",
134
- choices=["sse", "stdio"],
135
- default="sse",
134
+ choices=["sse", "http", "stdio"],
135
+ default="http",
136
136
  help="Transport protocol to use when running as a server (sse or stdio)",
137
137
  )
138
138
  parser.add_argument(
@@ -25,24 +25,23 @@ class RequestParams(CreateMessageRequestParams):
25
25
 
26
26
  model: str | None = None
27
27
  """
28
- The model to use for the LLM generation.
28
+ The model to use for the LLM generation. This can only be set during Agent creation.
29
29
  If specified, this overrides the 'modelPreferences' selection criteria.
30
30
  """
31
31
 
32
32
  use_history: bool = True
33
33
  """
34
- Include the message history in the generate request.
34
+ Agent/LLM maintains conversation history. Does not include applied Prompts
35
35
  """
36
36
 
37
- max_iterations: int = 10
37
+ max_iterations: int = 20
38
38
  """
39
- The maximum number of iterations to run the LLM for.
39
+ The maximum number of tool calls allowed in a conversation turn
40
40
  """
41
41
 
42
42
  parallel_tool_calls: bool = True
43
43
  """
44
- Whether to allow multiple tool calls per iteration.
45
- Also known as multi-step tool use.
44
+ Whether to allow simultaneous tool calls
46
45
  """
47
46
  response_format: Any | None = None
48
47
  """
@@ -274,7 +274,9 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
274
274
  # Calculate new conversation messages (excluding prompts)
275
275
  new_messages = messages[len(prompt_messages) :]
276
276
 
277
- # Update conversation history
277
+ if system_prompt:
278
+ new_messages = new_messages[1:]
279
+
278
280
  self.history.set(new_messages)
279
281
 
280
282
  self._log_chat_finished(model=self.default_request_params.model)
@@ -6,21 +6,16 @@ It adds logging and supports sampling requests.
6
6
  from datetime import timedelta
7
7
  from typing import TYPE_CHECKING, Optional
8
8
 
9
- from mcp import ClientSession
9
+ from mcp import ClientSession, ServerNotification
10
10
  from mcp.shared.session import (
11
- ReceiveNotificationT,
12
11
  ReceiveResultT,
13
12
  RequestId,
14
13
  SendNotificationT,
15
14
  SendRequestT,
16
15
  SendResultT,
17
16
  )
18
- from mcp.types import (
19
- ErrorData,
20
- ListRootsResult,
21
- Root,
22
- )
23
- from pydantic import AnyUrl
17
+ from mcp.types import ErrorData, ListRootsResult, Root, ToolListChangedNotification
18
+ from pydantic import FileUrl
24
19
 
25
20
  from mcp_agent.context_dependent import ContextDependent
26
21
  from mcp_agent.logging.logger import get_logger
@@ -45,7 +40,7 @@ async def list_roots(ctx: ClientSession) -> ListRootsResult:
45
40
  ):
46
41
  roots = [
47
42
  Root(
48
- uri=AnyUrl(
43
+ uri=FileUrl(
49
44
  root.server_uri_alias or root.uri,
50
45
  ),
51
46
  name=root.name,
@@ -67,6 +62,11 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
67
62
  """
68
63
 
69
64
  def __init__(self, *args, **kwargs) -> None:
65
+ # Extract server_name if provided in kwargs
66
+ self.session_server_name = kwargs.pop("server_name", None)
67
+ # Extract the notification callbacks if provided
68
+ self._tool_list_changed_callback = kwargs.pop("tool_list_changed_callback", None)
69
+
70
70
  super().__init__(*args, **kwargs, list_roots_callback=list_roots, sampling_callback=sample)
71
71
  self.server_config: Optional[MCPServerSettings] = None
72
72
 
@@ -104,7 +104,7 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
104
104
  )
105
105
  return await super()._send_response(request_id, response)
106
106
 
107
- async def _received_notification(self, notification: ReceiveNotificationT) -> None:
107
+ async def _received_notification(self, notification: ServerNotification) -> None:
108
108
  """
109
109
  Can be overridden by subclasses to handle a notification without needing
110
110
  to listen on the message stream.
@@ -113,7 +113,37 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
113
113
  "_received_notification: notification=",
114
114
  data=notification.model_dump(),
115
115
  )
116
- return await super()._received_notification(notification)
116
+
117
+ # Call parent notification handler first
118
+ await super()._received_notification(notification)
119
+
120
+ # Then process our specific notification types
121
+ match notification.root:
122
+ case ToolListChangedNotification():
123
+ # Simple notification handling - just call the callback if it exists
124
+ if self._tool_list_changed_callback and self.session_server_name:
125
+ logger.info(
126
+ f"Tool list changed for server '{self.session_server_name}', triggering callback"
127
+ )
128
+ # Use asyncio.create_task to prevent blocking the notification handler
129
+ import asyncio
130
+ asyncio.create_task(self._handle_tool_list_change_callback(self.session_server_name))
131
+ else:
132
+ logger.debug(
133
+ f"Tool list changed for server '{self.session_server_name}' but no callback registered"
134
+ )
135
+
136
+ return None
137
+
138
+ async def _handle_tool_list_change_callback(self, server_name: str) -> None:
139
+ """
140
+ Helper method to handle tool list change callback in a separate task
141
+ to prevent blocking the notification handler
142
+ """
143
+ try:
144
+ await self._tool_list_changed_callback(server_name)
145
+ except Exception as e:
146
+ logger.error(f"Error in tool list changed callback: {e}")
117
147
 
118
148
  async def send_progress_notification(
119
149
  self, progress_token: str | int, progress: float, total: float | None = None
@@ -138,6 +138,9 @@ class MCPAggregator(ContextDependent):
138
138
  self._prompt_cache: Dict[str, List[Prompt]] = {}
139
139
  self._prompt_cache_lock = Lock()
140
140
 
141
+ # Lock for refreshing tools from a server
142
+ self._refresh_lock = Lock()
143
+
141
144
  async def close(self) -> None:
142
145
  """
143
146
  Close all persistent connections when the aggregator is deleted.
@@ -217,8 +220,19 @@ class MCPAggregator(ContextDependent):
217
220
  },
218
221
  )
219
222
 
223
+ # Create a wrapper to capture the parameters for the client session
224
+ def session_factory(read_stream, write_stream, read_timeout):
225
+ return MCPAgentClientSession(
226
+ read_stream,
227
+ write_stream,
228
+ read_timeout,
229
+ server_name=server_name,
230
+ tool_list_changed_callback=self._handle_tool_list_changed
231
+ )
232
+
220
233
  await self._persistent_connection_manager.get_server(
221
- server_name, client_session_factory=MCPAgentClientSession
234
+ server_name,
235
+ client_session_factory=session_factory
222
236
  )
223
237
 
224
238
  logger.info(
@@ -261,8 +275,20 @@ class MCPAggregator(ContextDependent):
261
275
  tools = await fetch_tools(server_connection.session)
262
276
  prompts = await fetch_prompts(server_connection.session, server_name)
263
277
  else:
278
+ # Create a factory function for the client session
279
+ def create_session(read_stream, write_stream, read_timeout):
280
+ return MCPAgentClientSession(
281
+ read_stream,
282
+ write_stream,
283
+ read_timeout,
284
+ server_name=server_name,
285
+ tool_list_changed_callback=self._handle_tool_list_changed
286
+ )
287
+
264
288
  async with gen_client(
265
- server_name, server_registry=self.context.server_registry
289
+ server_name,
290
+ server_registry=self.context.server_registry,
291
+ client_session_factory=create_session
266
292
  ) as client:
267
293
  tools = await fetch_tools(client)
268
294
  prompts = await fetch_prompts(client, server_name)
@@ -384,6 +410,15 @@ class MCPAggregator(ContextDependent):
384
410
  ]
385
411
  )
386
412
 
413
+ async def refresh_all_tools(self) -> None:
414
+ """
415
+ Refresh the tools for all servers.
416
+ This is useful when you know tools have changed but haven't received notifications.
417
+ """
418
+ logger.info("Refreshing tools for all servers")
419
+ for server_name in self.server_names:
420
+ await self._refresh_server_tools(server_name)
421
+
387
422
  async def _execute_on_server(
388
423
  self,
389
424
  server_name: str,
@@ -864,6 +899,102 @@ class MCPAggregator(ContextDependent):
864
899
  logger.debug(f"Available prompts across servers: {results}")
865
900
  return results
866
901
 
902
+ async def _handle_tool_list_changed(self, server_name: str) -> None:
903
+ """
904
+ Callback handler for ToolListChangedNotification.
905
+ This will refresh the tools for the specified server.
906
+
907
+ Args:
908
+ server_name: The name of the server whose tools have changed
909
+ """
910
+ logger.info(f"Tool list changed for server '{server_name}', refreshing tools")
911
+
912
+ # Refresh the tools for this server
913
+ await self._refresh_server_tools(server_name)
914
+
915
+ async def _refresh_server_tools(self, server_name: str) -> None:
916
+ """
917
+ Refresh the tools for a specific server.
918
+
919
+ Args:
920
+ server_name: The name of the server to refresh tools for
921
+ """
922
+ if not await self.validate_server(server_name):
923
+ logger.error(f"Cannot refresh tools for unknown server '{server_name}'")
924
+ return
925
+
926
+ async with self._refresh_lock:
927
+ try:
928
+ # Fetch new tools from the server
929
+ if self.connection_persistence:
930
+ # Create a factory function that will include our parameters
931
+ def create_session(read_stream, write_stream, read_timeout):
932
+ return MCPAgentClientSession(
933
+ read_stream,
934
+ write_stream,
935
+ read_timeout,
936
+ server_name=server_name,
937
+ tool_list_changed_callback=self._handle_tool_list_changed
938
+ )
939
+
940
+ server_connection = await self._persistent_connection_manager.get_server(
941
+ server_name,
942
+ client_session_factory=create_session
943
+ )
944
+ tools_result = await server_connection.session.list_tools()
945
+ new_tools = tools_result.tools or []
946
+ else:
947
+ # Create a factory function for the client session
948
+ def create_session(read_stream, write_stream, read_timeout):
949
+ return MCPAgentClientSession(
950
+ read_stream,
951
+ write_stream,
952
+ read_timeout,
953
+ server_name=server_name,
954
+ tool_list_changed_callback=self._handle_tool_list_changed
955
+ )
956
+
957
+ async with gen_client(
958
+ server_name,
959
+ server_registry=self.context.server_registry,
960
+ client_session_factory=create_session
961
+ ) as client:
962
+ tools_result = await client.list_tools()
963
+ new_tools = tools_result.tools or []
964
+
965
+ # Update tool maps
966
+ async with self._tool_map_lock:
967
+ # Remove old tools for this server
968
+ old_tools = self._server_to_tool_map.get(server_name, [])
969
+ for old_tool in old_tools:
970
+ if old_tool.namespaced_tool_name in self._namespaced_tool_map:
971
+ del self._namespaced_tool_map[old_tool.namespaced_tool_name]
972
+
973
+ # Add new tools
974
+ self._server_to_tool_map[server_name] = []
975
+ for tool in new_tools:
976
+ namespaced_tool_name = create_namespaced_name(server_name, tool.name)
977
+ namespaced_tool = NamespacedTool(
978
+ tool=tool,
979
+ server_name=server_name,
980
+ namespaced_tool_name=namespaced_tool_name,
981
+ )
982
+
983
+ self._namespaced_tool_map[namespaced_tool_name] = namespaced_tool
984
+ self._server_to_tool_map[server_name].append(namespaced_tool)
985
+
986
+ logger.info(
987
+ f"Successfully refreshed tools for server '{server_name}'",
988
+ data={
989
+ "progress_action": ProgressAction.UPDATED,
990
+ "server_name": server_name,
991
+ "agent_name": self.agent_name,
992
+ "tool_count": len(new_tools),
993
+ },
994
+ )
995
+ except Exception as e:
996
+ logger.error(f"Failed to refresh tools for server '{server_name}': {e}")
997
+
867
998
  async def get_resource(
868
999
  self, resource_uri: str, server_name: str | None = None
869
1000
  ) -> ReadResourceResult:
@@ -23,6 +23,7 @@ from mcp.client.stdio import (
23
23
  get_default_environment,
24
24
  stdio_client,
25
25
  )
26
+ from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client
26
27
  from mcp.types import JSONRPCMessage, ServerCapabilities
27
28
 
28
29
  from mcp_agent.config import MCPServerSettings
@@ -40,6 +41,27 @@ if TYPE_CHECKING:
40
41
  logger = get_logger(__name__)
41
42
 
42
43
 
44
+ class StreamingContextAdapter:
45
+ """Adapter to provide a 3-value context from a 2-value context manager"""
46
+
47
+ def __init__(self, context_manager):
48
+ self.context_manager = context_manager
49
+ self.cm_instance = None
50
+
51
+ async def __aenter__(self):
52
+ self.cm_instance = await self.context_manager.__aenter__()
53
+ read_stream, write_stream = self.cm_instance
54
+ return read_stream, write_stream, None
55
+
56
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
57
+ return await self.context_manager.__aexit__(exc_type, exc_val, exc_tb)
58
+
59
+
60
+ def _add_none_to_context(context_manager):
61
+ """Helper to add a None value to context managers that return 2 values instead of 3"""
62
+ return StreamingContextAdapter(context_manager)
63
+
64
+
43
65
  class ServerConnection:
44
66
  """
45
67
  Represents a long-lived MCP server connection, including:
@@ -57,6 +79,7 @@ class ServerConnection:
57
79
  tuple[
58
80
  MemoryObjectReceiveStream[JSONRPCMessage | Exception],
59
81
  MemoryObjectSendStream[JSONRPCMessage],
82
+ GetSessionIdCallback | None,
60
83
  ],
61
84
  None,
62
85
  ],
@@ -162,7 +185,7 @@ async def _server_lifecycle_task(server_conn: ServerConnection) -> None:
162
185
  try:
163
186
  transport_context = server_conn._transport_context_factory()
164
187
 
165
- async with transport_context as (read_stream, write_stream):
188
+ async with transport_context as (read_stream, write_stream, _):
166
189
  server_conn.create_session(read_stream, write_stream)
167
190
 
168
191
  async with server_conn.session:
@@ -303,14 +326,17 @@ class MCPConnectionManager(ContextDependent):
303
326
  error_handler = get_stderr_handler(server_name)
304
327
  # Explicitly ensure we're using our custom logger for stderr
305
328
  logger.debug(f"{server_name}: Creating stdio client with custom error handler")
306
- return stdio_client(server_params, errlog=error_handler)
329
+ return _add_none_to_context(stdio_client(server_params, errlog=error_handler))
307
330
  elif config.transport == "sse":
308
- return sse_client(
309
- config.url,
310
- config.headers,
311
- sse_read_timeout=config.read_transport_sse_timeout_seconds,
331
+ return _add_none_to_context(
332
+ sse_client(
333
+ config.url,
334
+ config.headers,
335
+ sse_read_timeout=config.read_transport_sse_timeout_seconds,
336
+ )
312
337
  )
313
-
338
+ elif config.transport == "http":
339
+ return streamablehttp_client(config.url, config.headers)
314
340
  else:
315
341
  raise ValueError(f"Unsupported transport: {config.transport}")
316
342
 
@@ -335,7 +335,7 @@ def parse_args():
335
335
  parser.add_argument(
336
336
  "--transport",
337
337
  type=str,
338
- choices=["stdio", "sse"],
338
+ choices=["stdio", "sse", "http"],
339
339
  default="stdio",
340
340
  help="Transport to use (default: stdio)",
341
341
  )
@@ -502,14 +502,22 @@ async def async_main() -> int:
502
502
  return await test_prompt(args.test, config)
503
503
 
504
504
  # Start the server with the specified transport
505
- if config.transport == "stdio":
506
- await mcp.run_stdio_async()
507
- else: # sse
505
+ if config.transport == "sse": # sse
508
506
  # Set the host and port in settings before running the server
509
507
  mcp.settings.host = config.host
510
508
  mcp.settings.port = config.port
511
509
  logger.info(f"Starting SSE server on {config.host}:{config.port}")
512
510
  await mcp.run_sse_async()
511
+ elif config.transport == "http":
512
+ mcp.settings.host = config.host
513
+ mcp.settings.port = config.port
514
+ logger.info(f"Starting SSE server on {config.host}:{config.port}")
515
+ await mcp.run_streamable_http_async()
516
+ elif config.transport == "stdio":
517
+ await mcp.run_stdio_async()
518
+ else:
519
+ logger.error(f"Unknown transport: {config.transport}")
520
+ return 1
513
521
  return 0
514
522
 
515
523
 
@@ -140,9 +140,9 @@ class AgentMCPServer:
140
140
  print("Press Ctrl+C again to force exit.")
141
141
  self._graceful_shutdown_event.set()
142
142
 
143
- def run(self, transport: str = "sse", host: str = "0.0.0.0", port: int = 8000) -> None:
143
+ def run(self, transport: str = "http", host: str = "0.0.0.0", port: int = 8000) -> None:
144
144
  """Run the MCP server synchronously."""
145
- if transport == "sse":
145
+ if transport in ["sse", "http"]:
146
146
  self.mcp_server.settings.host = host
147
147
  self.mcp_server.settings.port = port
148
148
 
@@ -180,12 +180,12 @@ class AgentMCPServer:
180
180
  asyncio.run(self._cleanup_stdio())
181
181
 
182
182
  async def run_async(
183
- self, transport: str = "sse", host: str = "0.0.0.0", port: int = 8000
183
+ self, transport: str = "http", host: str = "0.0.0.0", port: int = 8000
184
184
  ) -> None:
185
185
  """Run the MCP server asynchronously with improved shutdown handling."""
186
186
  # Use different handling strategies based on transport type
187
- if transport == "sse":
188
- # For SSE, use our enhanced shutdown handling
187
+ if transport in ["sse", "http"]:
188
+ # For SSE/HTTP, use our enhanced shutdown handling
189
189
  self._setup_signal_handlers()
190
190
 
191
191
  self.mcp_server.settings.host = host
@@ -236,9 +236,9 @@ class AgentMCPServer:
236
236
 
237
237
  async def _run_server_with_shutdown(self, transport: str):
238
238
  """Run the server with proper shutdown handling."""
239
- # This method is only used for SSE transport
240
- if transport != "sse":
241
- raise ValueError("This method should only be used with SSE transport")
239
+ # This method is used for SSE/HTTP transport
240
+ if transport not in ["sse", "http"]:
241
+ raise ValueError("This method should only be used with SSE or HTTP transport")
242
242
 
243
243
  # Start a monitor task for shutdown
244
244
  shutdown_monitor = asyncio.create_task(self._monitor_shutdown())
@@ -262,8 +262,11 @@ class AgentMCPServer:
262
262
  # Replace with our tracking version
263
263
  self.mcp_server._sse_transport.connect_sse = tracked_connect_sse
264
264
 
265
- # Run the server (SSE only)
266
- await self.mcp_server.run_sse_async()
265
+ # Run the server based on transport type
266
+ if transport == "sse":
267
+ await self.mcp_server.run_sse_async()
268
+ elif transport == "http":
269
+ await self.mcp_server.run_streamable_http_async()
267
270
  finally:
268
271
  # Cancel the monitor when the server exits
269
272
  shutdown_monitor.cancel()
@@ -18,6 +18,7 @@ from mcp.client.stdio import (
18
18
  StdioServerParameters,
19
19
  get_default_environment,
20
20
  )
21
+ from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client
21
22
 
22
23
  from mcp_agent.config import (
23
24
  MCPServerAuthSettings,
@@ -27,7 +28,10 @@ from mcp_agent.config import (
27
28
  )
28
29
  from mcp_agent.logging.logger import get_logger
29
30
  from mcp_agent.mcp.logger_textio import get_stderr_handler
30
- from mcp_agent.mcp.mcp_connection_manager import MCPConnectionManager
31
+ from mcp_agent.mcp.mcp_connection_manager import (
32
+ MCPConnectionManager,
33
+ _add_none_to_context,
34
+ )
31
35
 
32
36
  logger = get_logger(__name__)
33
37
 
@@ -93,7 +97,12 @@ class ServerRegistry:
93
97
  self,
94
98
  server_name: str,
95
99
  client_session_factory: Callable[
96
- [MemoryObjectReceiveStream, MemoryObjectSendStream, timedelta | None],
100
+ [
101
+ MemoryObjectReceiveStream,
102
+ MemoryObjectSendStream,
103
+ timedelta | None,
104
+ GetSessionIdCallback | None,
105
+ ],
97
106
  ClientSession,
98
107
  ] = ClientSession,
99
108
  ) -> AsyncGenerator[ClientSession, None]:
@@ -132,14 +141,18 @@ class ServerRegistry:
132
141
  )
133
142
 
134
143
  # Create a stderr handler that logs to our application logger
135
- async with stdio_client(server_params, errlog=get_stderr_handler(server_name)) as (
144
+ async with _add_none_to_context(
145
+ stdio_client(server_params, errlog=get_stderr_handler(server_name))
146
+ ) as (
136
147
  read_stream,
137
148
  write_stream,
149
+ _,
138
150
  ):
139
151
  session = client_session_factory(
140
152
  read_stream,
141
153
  write_stream,
142
154
  read_timeout_seconds,
155
+ None, # No callback for stdio
143
156
  )
144
157
  async with session:
145
158
  logger.info(f"{server_name}: Connected to server using stdio transport.")
@@ -153,15 +166,18 @@ class ServerRegistry:
153
166
  raise ValueError(f"URL is required for SSE transport: {server_name}")
154
167
 
155
168
  # Use sse_client to get the read and write streams
156
- async with sse_client(
157
- config.url,
158
- config.headers,
159
- sse_read_timeout=config.read_transport_sse_timeout_seconds,
160
- ) as (read_stream, write_stream):
169
+ async with _add_none_to_context(
170
+ sse_client(
171
+ config.url,
172
+ config.headers,
173
+ sse_read_timeout=config.read_transport_sse_timeout_seconds,
174
+ )
175
+ ) as (read_stream, write_stream, _):
161
176
  session = client_session_factory(
162
177
  read_stream,
163
178
  write_stream,
164
179
  read_timeout_seconds,
180
+ None, # No callback for stdio
165
181
  )
166
182
  async with session:
167
183
  logger.info(f"{server_name}: Connected to server using SSE transport.")
@@ -169,6 +185,27 @@ class ServerRegistry:
169
185
  yield session
170
186
  finally:
171
187
  logger.debug(f"{server_name}: Closed session to server")
188
+ elif config.transport == "http":
189
+ if not config.url:
190
+ raise ValueError(f"URL is required for SSE transport: {server_name}")
191
+
192
+ async with streamablehttp_client(config.url, config.headers) as (
193
+ read_stream,
194
+ write_stream,
195
+ _,
196
+ ):
197
+ session = client_session_factory(
198
+ read_stream,
199
+ write_stream,
200
+ read_timeout_seconds,
201
+ None, # No callback for stdio
202
+ )
203
+ async with session:
204
+ logger.info(f"{server_name}: Connected to server using HTTP transport.")
205
+ try:
206
+ yield session
207
+ finally:
208
+ logger.debug(f"{server_name}: Closed session to server")
172
209
 
173
210
  # Unsupported transport
174
211
  else:
@@ -179,7 +216,12 @@ class ServerRegistry:
179
216
  self,
180
217
  server_name: str,
181
218
  client_session_factory: Callable[
182
- [MemoryObjectReceiveStream, MemoryObjectSendStream, timedelta | None],
219
+ [
220
+ MemoryObjectReceiveStream,
221
+ MemoryObjectSendStream,
222
+ timedelta | None,
223
+ GetSessionIdCallback,
224
+ ],
183
225
  ClientSession,
184
226
  ] = ClientSession,
185
227
  init_hook: InitHookCallable = None,
@@ -23,5 +23,5 @@ logger:
23
23
  mcp:
24
24
  servers:
25
25
  agent_one:
26
- transport: sse
27
- url: http://localhost:8001/sse
26
+ transport: http
27
+ url: http://localhost:8001/mcp
@@ -25,6 +25,7 @@ class ConsoleDisplay:
25
25
  config: Configuration object containing display preferences
26
26
  """
27
27
  self.config = config
28
+ self._markup = config.logger.enable_markup if config else True
28
29
 
29
30
  def show_tool_result(self, result: CallToolResult) -> None:
30
31
  """Display a tool result in a formatted panel."""
@@ -46,7 +47,7 @@ class ConsoleDisplay:
46
47
  if len(str(result.content)) > 360:
47
48
  panel.height = 8
48
49
 
49
- console.console.print(panel)
50
+ console.console.print(panel, markup=self._markup)
50
51
  console.console.print("\n")
51
52
 
52
53
  def show_oai_tool_result(self, result) -> None:
@@ -67,7 +68,7 @@ class ConsoleDisplay:
67
68
  if len(str(result)) > 360:
68
69
  panel.height = 8
69
70
 
70
- console.console.print(panel)
71
+ console.console.print(panel, markup=self._markup)
71
72
  console.console.print("\n")
72
73
 
73
74
  def show_tool_call(self, available_tools, tool_name, tool_args) -> None:
@@ -92,7 +93,7 @@ class ConsoleDisplay:
92
93
  if len(str(tool_args)) > 360:
93
94
  panel.height = 8
94
95
 
95
- console.console.print(panel)
96
+ console.console.print(panel, markup=self._markup)
96
97
  console.console.print("\n")
97
98
 
98
99
  def _format_tool_list(self, available_tools, selected_tool_name):
@@ -172,7 +173,7 @@ class ConsoleDisplay:
172
173
  subtitle=display_server_list,
173
174
  subtitle_align="left",
174
175
  )
175
- console.console.print(panel)
176
+ console.console.print(panel, markup=self._markup)
176
177
  console.console.print("\n")
177
178
 
178
179
  def show_user_message(
@@ -196,7 +197,7 @@ class ConsoleDisplay:
196
197
  subtitle=subtitle_text,
197
198
  subtitle_align="left",
198
199
  )
199
- console.console.print(panel)
200
+ console.console.print(panel, markup=self._markup)
200
201
  console.console.print("\n")
201
202
 
202
203
  async def show_prompt_loaded(
@@ -270,5 +271,5 @@ class ConsoleDisplay:
270
271
  subtitle_align="left",
271
272
  )
272
273
 
273
- console.console.print(panel)
274
+ console.console.print(panel, markup=self._markup)
274
275
  console.console.print("\n")