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.
- {fast_agent_mcp-0.2.22.dist-info → fast_agent_mcp-0.2.24.dist-info}/METADATA +3 -2
- {fast_agent_mcp-0.2.22.dist-info → fast_agent_mcp-0.2.24.dist-info}/RECORD +21 -20
- mcp_agent/agents/workflow/orchestrator_agent.py +2 -2
- mcp_agent/cli/commands/go.py +94 -29
- mcp_agent/cli/commands/url_parser.py +185 -0
- mcp_agent/config.py +3 -1
- mcp_agent/context.py +2 -0
- mcp_agent/core/fastagent.py +2 -2
- mcp_agent/core/request_params.py +5 -6
- mcp_agent/llm/providers/augmented_llm_openai.py +3 -1
- mcp_agent/mcp/mcp_agent_client_session.py +41 -11
- mcp_agent/mcp/mcp_aggregator.py +133 -2
- mcp_agent/mcp/mcp_connection_manager.py +33 -7
- mcp_agent/mcp/prompts/prompt_server.py +12 -4
- mcp_agent/mcp_server/agent_server.py +13 -10
- mcp_agent/mcp_server_registry.py +51 -9
- mcp_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +2 -2
- mcp_agent/ui/console_display.py +7 -6
- {fast_agent_mcp-0.2.22.dist-info → fast_agent_mcp-0.2.24.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.22.dist-info → fast_agent_mcp-0.2.24.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.2.22.dist-info → fast_agent_mcp-0.2.24.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,8 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fast-agent-mcp
|
3
|
-
Version: 0.2.
|
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
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
89
|
-
mcp_agent/mcp/mcp_aggregator.py,sha256=
|
90
|
-
mcp_agent/mcp/mcp_connection_manager.py,sha256=
|
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=
|
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=
|
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
|
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=
|
148
|
-
fast_agent_mcp-0.2.
|
149
|
-
fast_agent_mcp-0.2.
|
150
|
-
fast_agent_mcp-0.2.
|
151
|
-
fast_agent_mcp-0.2.
|
152
|
-
fast_agent_mcp-0.2.
|
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.
|
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, "
|
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
|
mcp_agent/cli/commands/go.py
CHANGED
@@ -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
|
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(
|
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(
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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:
|
mcp_agent/core/fastagent.py
CHANGED
@@ -131,8 +131,8 @@ class FastAgent:
|
|
131
131
|
)
|
132
132
|
parser.add_argument(
|
133
133
|
"--transport",
|
134
|
-
choices=["sse", "stdio"],
|
135
|
-
default="
|
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(
|
mcp_agent/core/request_params.py
CHANGED
@@ -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
|
-
|
34
|
+
Agent/LLM maintains conversation history. Does not include applied Prompts
|
35
35
|
"""
|
36
36
|
|
37
|
-
max_iterations: int =
|
37
|
+
max_iterations: int = 20
|
38
38
|
"""
|
39
|
-
The maximum number of
|
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
|
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
|
-
|
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
|
-
|
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=
|
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:
|
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
|
-
|
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
|
mcp_agent/mcp/mcp_aggregator.py
CHANGED
@@ -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,
|
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,
|
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
|
309
|
-
|
310
|
-
|
311
|
-
|
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 == "
|
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 = "
|
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
|
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 = "
|
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
|
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
|
240
|
-
if transport
|
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
|
266
|
-
|
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()
|
mcp_agent/mcp_server_registry.py
CHANGED
@@ -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
|
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
|
-
[
|
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
|
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
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
[
|
219
|
+
[
|
220
|
+
MemoryObjectReceiveStream,
|
221
|
+
MemoryObjectSendStream,
|
222
|
+
timedelta | None,
|
223
|
+
GetSessionIdCallback,
|
224
|
+
],
|
183
225
|
ClientSession,
|
184
226
|
] = ClientSession,
|
185
227
|
init_hook: InitHookCallable = None,
|
mcp_agent/ui/console_display.py
CHANGED
@@ -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")
|
File without changes
|
File without changes
|
File without changes
|