fast-agent-mcp 0.2.21__py3-none-any.whl → 0.2.22__py3-none-any.whl

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

Potentially problematic release.


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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fast-agent-mcp
3
- Version: 0.2.21
3
+ Version: 0.2.22
4
4
  Summary: Define, Prompt and Test MCP enabled Agents and Workflows
5
5
  Author-email: Shaun Smith <fastagent@llmindset.co.uk>, Sarmad Qadri <sarmad@lastmileai.dev>
6
6
  License: Apache License
@@ -213,7 +213,7 @@ Requires-Dist: a2a-types>=0.1.0
213
213
  Requires-Dist: aiohttp>=3.11.13
214
214
  Requires-Dist: anthropic>=0.49.0
215
215
  Requires-Dist: fastapi>=0.115.6
216
- Requires-Dist: mcp~=1.7.0
216
+ Requires-Dist: mcp>=1.8.0
217
217
  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
@@ -224,6 +224,7 @@ Requires-Dist: pydantic-settings>=2.7.0
224
224
  Requires-Dist: pydantic>=2.10.4
225
225
  Requires-Dist: pyyaml>=6.0.2
226
226
  Requires-Dist: rich>=13.9.4
227
+ Requires-Dist: tensorzero>=2025.4.7
227
228
  Requires-Dist: typer>=0.15.1
228
229
  Provides-Extra: dev
229
230
  Requires-Dist: anthropic>=0.42.0; extra == 'dev'
@@ -260,6 +261,7 @@ The simple declarative syntax lets you concentrate on composing your Prompts and
260
261
  `fast-agent` is multi-modal, supporting Images and PDFs for both Anthropic and OpenAI endpoints via Prompts, Resources and MCP Tool Call results. The inclusion of passthrough and playback LLMs enable rapid development and test of Python glue-code for your applications.
261
262
 
262
263
  > [!IMPORTANT]
264
+ >
263
265
  > `fast-agent` The fast-agent documentation repo is here: https://github.com/evalstate/fast-agent-docs. Please feel free to submit PRs for documentation, experience reports or other content you think others may find helpful. All help and feedback warmly received.
264
266
 
265
267
  ### Agent Application Development
@@ -277,12 +279,12 @@ Simple model selection makes testing Model <-> MCP Server interaction painless.
277
279
  Start by installing the [uv package manager](https://docs.astral.sh/uv/) for Python. Then:
278
280
 
279
281
  ```bash
280
- uv pip install fast-agent-mcp # install fast-agent!
282
+ uv pip install fast-agent-mcp # install fast-agent!
281
283
 
282
- fast-agent setup # create an example agent and config files
283
- uv run agent.py # run your first agent
284
- uv run agent.py --model=o3-mini.low # specify a model
285
- fast-agent quickstart workflow # create "building effective agents" examples
284
+ uv run fast-agent setup # create an example agent and config files
285
+ uv run agent.py # run your first agent
286
+ uv run agent.py --model=o3-mini.low # specify a model
287
+ uv run fast-agent quickstart workflow # create "building effective agents" examples
286
288
  ```
287
289
 
288
290
  Other quickstart examples include a Researcher Agent (with Evaluator-Optimizer workflow) and Data Analysis Agent (similar to the ChatGPT experience), demonstrating MCP Roots support.
@@ -1,10 +1,10 @@
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=_b5JeS2nWHScSUUTu6wYxXzdfKefoqII305ecKcw7Gs,12248
3
+ mcp_agent/config.py,sha256=ZC4SiIVbxVn7-hUfv3RFj6fNrXxvci6gmUNCGM7vzs8,12624
4
4
  mcp_agent/console.py,sha256=Gjf2QLFumwG1Lav__c07X_kZxxEUSkzV-1_-YbAwcwo,813
5
5
  mcp_agent/context.py,sha256=Kb3s_0MolHx7AeTs1NVcY3ly-xFBd35o8LT7Srpx9is,7334
6
6
  mcp_agent/context_dependent.py,sha256=QXfhw3RaQCKfscEEBRGuZ3sdMWqkgShz2jJ1ivGGX1I,1455
7
- mcp_agent/event_progress.py,sha256=3dqk5Pn1tAG_m_wn4IPNwLWLyzm7CyKIidqHN-4l-JY,2736
7
+ mcp_agent/event_progress.py,sha256=b1VKlQQF2AgPMb6XHjlJAVoPdx8GuxRTUk2g-4lBNm0,2749
8
8
  mcp_agent/mcp_server_registry.py,sha256=jUmCdfcpTitXm1-3TxpWsdRWY_8phdKNYgXwB16ZSVU,10100
9
9
  mcp_agent/progress_display.py,sha256=GeJU9VUt6qKsFVymG688hCMVCsAygG9ifiiEb5IcbN4,361
10
10
  mcp_agent/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -23,7 +23,7 @@ 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=DJpmq4n-p5r8BXH10UqBOexmLND-zSODl5f-w4noR5Q,4304
26
+ mcp_agent/cli/commands/go.py,sha256=2UY8TSDwhh_-p-WYXrZz3pEv3-2eTdBl5Lxy3JyJV0E,6057
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
29
  mcp_agent/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -38,7 +38,7 @@ mcp_agent/core/fastagent.py,sha256=WEEGz2WBAddDGNeWJwqwFIPLiQnLjaNxZLoMR0peyyU,2
38
38
  mcp_agent/core/interactive_prompt.py,sha256=w3VyRzW4hzn0xhWZRwo_qRRAD5WVSrJYe8QDe1XZ55Y,24252
39
39
  mcp_agent/core/mcp_content.py,sha256=2D7KHY9mG_vxoDwFLKvsPQV9VRIzHItM7V-jcEnACh8,8878
40
40
  mcp_agent/core/prompt.py,sha256=qnintOUGEoDPYLI9bu9G2OlgVMCe5ZPUZilgMzydXhc,7919
41
- mcp_agent/core/request_params.py,sha256=loYf13DN7e-DsdYRd37jWkJWJGwVBL-iFkcANP1J60Q,1366
41
+ mcp_agent/core/request_params.py,sha256=vRfAz9T6Ir-0oeJ4qEdO62LDOzoLwBuuXcBcdh6WPZ8,1576
42
42
  mcp_agent/core/validation.py,sha256=RIBKFlh0GJg4rTcFQXoXp8A0sK1HpsCigKcYSK3gFaY,12090
43
43
  mcp_agent/executor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
44
  mcp_agent/executor/executor.py,sha256=E44p6d-o3OMRoP_dNs_cDnyti91LQ3P9eNU88mSi1kc,9462
@@ -48,26 +48,28 @@ mcp_agent/human_input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
48
48
  mcp_agent/human_input/handler.py,sha256=s712Z5ssTCwjL9-VKoIdP5CtgMh43YvepynYisiWTTA,3144
49
49
  mcp_agent/human_input/types.py,sha256=RtWBOVzy8vnYoQrc36jRLn8z8N3C4pDPMBN5vF6qM5Y,1476
50
50
  mcp_agent/llm/__init__.py,sha256=d8zgwG-bRFuwiMNMYkywg_qytk4P8lawyld_meuUmHI,68
51
- mcp_agent/llm/augmented_llm.py,sha256=ASe604OhrMZ9dVoGEUEpUQaY6fFamz4gL8ttzWP_9m0,24212
51
+ mcp_agent/llm/augmented_llm.py,sha256=CqtSGo_QrHE73tz_DHMd0wdt2F41gwuUu5Bue51FNm4,24199
52
52
  mcp_agent/llm/augmented_llm_passthrough.py,sha256=zHcctNpwg4EFJvD1x9Eg443SVX-uyzFphLikwF_yVE0,6288
53
53
  mcp_agent/llm/augmented_llm_playback.py,sha256=6L_RWIK__R67oZK7u3Xt3hWy1T2LnHXIO-efqgP3tPw,4177
54
54
  mcp_agent/llm/memory.py,sha256=HQ_c1QemOUjrkY6Z2omE6BG5fXga7y4jN7KCMOuGjPs,3345
55
- mcp_agent/llm/model_factory.py,sha256=QK3RrEhpvafKRlTFYR8Z9oNJRGjO7J9wIx5TGcDu6As,7649
55
+ mcp_agent/llm/model_factory.py,sha256=h3NJSa0yPa9iiLojEqBhIm9wgEBB46ZBibe44MnskHM,8089
56
56
  mcp_agent/llm/prompt_utils.py,sha256=yWQHykoK13QRF7evHUKxVF0SpVLN-Bsft0Yixzvn0g0,4825
57
57
  mcp_agent/llm/provider_key_manager.py,sha256=-K_FuibN6hdSnweT32lB8mKTfCARnbja6zYYs0ErTKg,2802
58
- mcp_agent/llm/provider_types.py,sha256=3IRRt9UbIHZHVJq1KF1XYfzsQtF9gj_gBNtU3hukIaY,308
58
+ mcp_agent/llm/provider_types.py,sha256=oWwXTlyr6hIzU_QLJ5T-UwxZGo5e4Pjwtahz2cr1PHg,364
59
59
  mcp_agent/llm/sampling_converter.py,sha256=C7wPBlmT0eD90XWabC22zkxsrVHKCrjwIwg6cG628cI,2926
60
60
  mcp_agent/llm/sampling_format_converter.py,sha256=xGz4odHpOcP7--eFaJaFtUR8eR9jxZS7MnLH6J7n0EU,1263
61
61
  mcp_agent/llm/providers/__init__.py,sha256=heVxtmuqFJOnjjxHz4bWSqTAxXoN1E8twC_gQ_yJpHk,265
62
62
  mcp_agent/llm/providers/anthropic_utils.py,sha256=vYDN5G5jKMhD2CQg8veJYab7tvvzYkDMq8M1g_hUAQg,3275
63
- mcp_agent/llm/providers/augmented_llm_anthropic.py,sha256=RQ4r5Q84VJ_dyuNo23b-EMzvq6RrpspzIQWtfVUfw6M,15468
63
+ mcp_agent/llm/providers/augmented_llm_anthropic.py,sha256=gK_IvllVBNJUUrSfpgFpdhM-d4liCt0MLq7d2lXS7RI,15510
64
64
  mcp_agent/llm/providers/augmented_llm_deepseek.py,sha256=NiZK5nv91ZS2VgVFXpbsFNFYLsLcppcbo_RstlRMd7I,1145
65
65
  mcp_agent/llm/providers/augmented_llm_generic.py,sha256=5Uq8ZBhcFuQTt7koP_5ykolREh2iWu8zKhNbh3pM9lQ,1210
66
66
  mcp_agent/llm/providers/augmented_llm_google.py,sha256=N0a2fphVtkvNYxKQpEX6J4tlO1C_mRw4sw3LBXnrOeI,1130
67
- mcp_agent/llm/providers/augmented_llm_openai.py,sha256=TumZs1y678IvyvYIehf8xSDqYWqC44dWrIbqFGtz03g,14085
67
+ mcp_agent/llm/providers/augmented_llm_openai.py,sha256=0C7BOB7i3xo0HsMCTagRSQ8Hsywb-31mot26OfohzCU,14478
68
68
  mcp_agent/llm/providers/augmented_llm_openrouter.py,sha256=V_TlVKm92GHBxYIo6gpvH_6cAaIdppS25Tz6x5T7LW0,2341
69
+ mcp_agent/llm/providers/augmented_llm_tensorzero.py,sha256=Mol_Wzj_ZtccW-LMw0oFwWUt1m1yfofloay9QYNP23c,20729
69
70
  mcp_agent/llm/providers/multipart_converter_anthropic.py,sha256=t5lHYGfFUacJldnrVtMNW-8gEMoto8Y7hJkDrnyZR-Y,16650
70
71
  mcp_agent/llm/providers/multipart_converter_openai.py,sha256=XPIulWntNpZWNGWrc240StPzok2RqrDAV7OigDwQ1uU,15850
72
+ mcp_agent/llm/providers/multipart_converter_tensorzero.py,sha256=BFTdyVk42HZskDAuTHicfDTUJq89d1fz8C9nAOuHxlE,8646
71
73
  mcp_agent/llm/providers/openai_multipart.py,sha256=qKBn7d3jSabnJmVgWweVzqh8q9mBqr09fsPmP92niAQ,6899
72
74
  mcp_agent/llm/providers/openai_utils.py,sha256=T4bTCL9f7DsoS_zoKgQKv_FUv_4n98vgbvaUpdWZJr8,1875
73
75
  mcp_agent/llm/providers/sampling_converter_anthropic.py,sha256=35WzBWkPklnuMlu5S6XsQIq0YL58NOy8Ja6A_l4m6eM,1612
@@ -85,7 +87,7 @@ mcp_agent/mcp/interfaces.py,sha256=PAou8znAl2HgtvfCpLQOZFbKra9F72OcVRfBJbboNX8,6
85
87
  mcp_agent/mcp/logger_textio.py,sha256=vljC1BtNTCxBAda9ExqNB-FwVNUZIuJT3h1nWmCjMws,3172
86
88
  mcp_agent/mcp/mcp_agent_client_session.py,sha256=Ng7epBXq8BEA_3m1GX5LqwafgNUAMSzBugwN6N0VUWQ,4364
87
89
  mcp_agent/mcp/mcp_aggregator.py,sha256=lVSt0yp0CnaYjcHCWmluwBeFgl8JXHYEZk0MzXgrQzA,40110
88
- mcp_agent/mcp/mcp_connection_manager.py,sha256=L5Dk4cyarN_v2rfktkrfZJR4xUuD3yN_hUyQnKHBWgM,14044
90
+ mcp_agent/mcp/mcp_connection_manager.py,sha256=6jtjclh4YNJZsNwYnSWmQ6cPzapAwsRUxir1c_gVNfM,16051
89
91
  mcp_agent/mcp/mime_utils.py,sha256=difepNR_gpb4MpMLkBRAoyhDk-AjXUHTiqKvT_VwS1o,1805
90
92
  mcp_agent/mcp/prompt_message_multipart.py,sha256=BDwRdNwyWHb2q2bccDb2iR2VlORqVvkvoG3xYzcMpCE,4403
91
93
  mcp_agent/mcp/prompt_render.py,sha256=k3v4BZDThGE2gGiOYVQtA6x8WTEdOuXIEnRafANhN1U,2996
@@ -143,8 +145,8 @@ mcp_agent/resources/examples/workflows/parallel.py,sha256=DQ5vY5-h8Qa5QHcYjsWXhZ
143
145
  mcp_agent/resources/examples/workflows/router.py,sha256=E4x_-c3l4YW9w1i4ARcDtkdeqIdbWEGfsMzwLYpdbVc,1677
144
146
  mcp_agent/resources/examples/workflows/short_story.txt,sha256=X3y_1AyhLFN2AKzCKvucJtDgAFIJfnlbsbGZO5bBWu0,1187
145
147
  mcp_agent/ui/console_display.py,sha256=TVGDtJ37hc6UG0ei9g7ZPZZfFNeS1MYozt-Mx8HsPCk,9752
146
- fast_agent_mcp-0.2.21.dist-info/METADATA,sha256=c-Aq5664t9xi4iW2mcDHUGaDL_jRUznn_nyeMVFJzgY,30142
147
- fast_agent_mcp-0.2.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
148
- fast_agent_mcp-0.2.21.dist-info/entry_points.txt,sha256=bRniFM5zk3Kix5z7scX0gf9VnmGQ2Cz_Q1Gh7Ir4W00,186
149
- fast_agent_mcp-0.2.21.dist-info/licenses/LICENSE,sha256=cN3FxDURL9XuzE5mhK9L2paZo82LTfjwCYVT7e3j0e4,10939
150
- fast_agent_mcp-0.2.21.dist-info/RECORD,,
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,,
@@ -18,8 +18,13 @@ async def _run_agent(
18
18
  config_path: Optional[str] = None,
19
19
  server_list: Optional[List[str]] = None,
20
20
  model: Optional[str] = None,
21
+ message: Optional[str] = None,
22
+ prompt_file: Optional[str] = None
21
23
  ) -> None:
22
24
  """Async implementation to run an interactive agent."""
25
+ from pathlib import Path
26
+
27
+ from mcp_agent.mcp.prompts.prompt_load import load_prompt_multipart
23
28
 
24
29
  # Create the FastAgent instance with CLI arg parsing enabled
25
30
  # It will automatically parse args like --model, --quiet, etc.
@@ -27,6 +32,7 @@ async def _run_agent(
27
32
  "name": name,
28
33
  "config_path": config_path,
29
34
  "ignore_unknown_args": True,
35
+ "parse_cli_args": False, # Don't parse CLI args, we're handling it ourselves
30
36
  }
31
37
 
32
38
  fast = FastAgent(**fast_kwargs)
@@ -38,10 +44,26 @@ async def _run_agent(
38
44
  if model:
39
45
  agent_kwargs["model"] = model
40
46
 
41
- @fast.agent(**agent_kwargs)
42
- async def cli_agent():
43
- async with fast.run() as agent:
44
- await agent.interactive()
47
+ # Handle prompt file and message options
48
+ if message or prompt_file:
49
+ @fast.agent(**agent_kwargs)
50
+ async def cli_agent():
51
+ async with fast.run() as agent:
52
+ if message:
53
+ response = await agent.send(message)
54
+ # Print the response and exit
55
+ print(response)
56
+ elif prompt_file:
57
+ prompt = load_prompt_multipart(Path(prompt_file))
58
+ response = await agent.generate(prompt)
59
+ # Print the response text and exit
60
+ print(response.last_text())
61
+ else:
62
+ # Standard interactive mode
63
+ @fast.agent(**agent_kwargs)
64
+ async def cli_agent():
65
+ async with fast.run() as agent:
66
+ await agent.interactive()
45
67
 
46
68
  # Run the agent
47
69
  await cli_agent()
@@ -51,7 +73,9 @@ def run_async_agent(
51
73
  instruction: str,
52
74
  config_path: Optional[str] = None,
53
75
  servers: Optional[str] = None,
54
- model: Optional[str] = None
76
+ model: Optional[str] = None,
77
+ message: Optional[str] = None,
78
+ prompt_file: Optional[str] = None
55
79
  ):
56
80
  """Run the async agent function with proper loop handling."""
57
81
  server_list = servers.split(',') if servers else None
@@ -75,7 +99,9 @@ def run_async_agent(
75
99
  instruction=instruction,
76
100
  config_path=config_path,
77
101
  server_list=server_list,
78
- model=model
102
+ model=model,
103
+ message=message,
104
+ prompt_file=prompt_file
79
105
  ))
80
106
  finally:
81
107
  try:
@@ -108,26 +134,38 @@ def go(
108
134
  model: Optional[str] = typer.Option(
109
135
  None, "--model", help="Override the default model (e.g., haiku, sonnet, gpt-4)"
110
136
  ),
137
+ message: Optional[str] = typer.Option(
138
+ None, "--message", "-m", help="Message to send to the agent (skips interactive mode)"
139
+ ),
140
+ prompt_file: Optional[str] = typer.Option(
141
+ None, "--prompt-file", "-p", help="Path to a prompt file to use (either text or JSON)"
142
+ ),
111
143
  ) -> None:
112
144
  """
113
145
  Run an interactive agent directly from the command line.
114
146
 
115
- Example:
147
+ Examples:
116
148
  fast-agent go --model=haiku --instruction="You are a coding assistant" --servers=fetch,filesystem
149
+ fast-agent go --message="What is the weather today?" --model=haiku
150
+ fast-agent go --prompt-file=my-prompt.txt --model=haiku
117
151
 
118
152
  This will start an interactive session with the agent, using the specified model
119
153
  and instruction. It will use the default configuration from fastagent.config.yaml
120
154
  unless --config-path is specified.
121
155
 
122
156
  Common options:
123
- --model: Override the default model (e.g., --model=haiku)
124
- --quiet: Disable progress display and logging
125
- --servers: Comma-separated list of server names to enable from config
157
+ --model Override the default model (e.g., --model=haiku)
158
+ --quiet Disable progress display and logging
159
+ --servers Comma-separated list of server names to enable from config
160
+ --message, -m Send a single message and exit
161
+ --prompt-file, -p Use a prompt file instead of interactive mode
126
162
  """
127
163
  run_async_agent(
128
164
  name=name,
129
165
  instruction=instruction,
130
166
  config_path=config_path,
131
167
  servers=servers,
132
- model=model
168
+ model=model,
169
+ message=message,
170
+ prompt_file=prompt_file
133
171
  )
mcp_agent/config.py CHANGED
@@ -198,6 +198,16 @@ class OpenTelemetrySettings(BaseModel):
198
198
  """Sample rate for tracing (1.0 = sample everything)"""
199
199
 
200
200
 
201
+ class TensorZeroSettings(BaseModel):
202
+ """
203
+ Settings for using TensorZero via its OpenAI-compatible API.
204
+ """
205
+
206
+ base_url: Optional[str] = None
207
+ api_key: Optional[str] = None
208
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
209
+
210
+
201
211
  class LoggerSettings(BaseModel):
202
212
  """
203
213
  Logger settings for the fast-agent application.
@@ -287,6 +297,9 @@ class Settings(BaseSettings):
287
297
  generic: GenericSettings | None = None
288
298
  """Settings for using Generic models in the fast-agent application"""
289
299
 
300
+ tensorzero: Optional[TensorZeroSettings] = None
301
+ """Settings for using TensorZero inference gateway"""
302
+
290
303
  logger: LoggerSettings | None = LoggerSettings()
291
304
  """Logger settings for the fast-agent application"""
292
305
 
@@ -2,7 +2,7 @@
2
2
  Request parameters definitions for LLM interactions.
3
3
  """
4
4
 
5
- from typing import Any, List
5
+ from typing import Any, Dict, List
6
6
 
7
7
  from mcp import SamplingMessage
8
8
  from mcp.types import CreateMessageRequestParams
@@ -48,3 +48,8 @@ class RequestParams(CreateMessageRequestParams):
48
48
  """
49
49
  Override response format for structured calls. Prefer sending pydantic model - only use in exceptional circumstances
50
50
  """
51
+
52
+ template_vars: Dict[str, Any] = Field(default_factory=dict)
53
+ """
54
+ Optional dictionary of template variables for dynamic templates. Currently only works for TensorZero inference backend
55
+ """
@@ -88,7 +88,7 @@ def convert_log_event(event: Event) -> Optional[ProgressEvent]:
88
88
 
89
89
  return ProgressEvent(
90
90
  action=ProgressAction(progress_action),
91
- target=target,
91
+ target=target or "unknown",
92
92
  details=details,
93
93
  agent_name=event_data.get("agent_name"),
94
94
  )
@@ -76,20 +76,14 @@ def deep_merge(dict1: Dict[Any, Any], dict2: Dict[Any, Any]) -> Dict[Any, Any]:
76
76
  Dict: The updated `dict1`.
77
77
  """
78
78
  for key in dict2:
79
- if (
80
- key in dict1
81
- and isinstance(dict1[key], dict)
82
- and isinstance(dict2[key], dict)
83
- ):
79
+ if key in dict1 and isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
84
80
  deep_merge(dict1[key], dict2[key])
85
81
  else:
86
82
  dict1[key] = dict2[key]
87
83
  return dict1
88
84
 
89
85
 
90
- class AugmentedLLM(
91
- ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT, MessageT]
92
- ):
86
+ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT, MessageT]):
93
87
  # Common parameter names used across providers
94
88
  PARAM_MESSAGES = "messages"
95
89
  PARAM_MODEL = "model"
@@ -100,7 +94,7 @@ class AugmentedLLM(
100
94
  PARAM_METADATA = "metadata"
101
95
  PARAM_USE_HISTORY = "use_history"
102
96
  PARAM_MAX_ITERATIONS = "max_iterations"
103
-
97
+ PARAM_TEMPLATE_VARS = "template_vars"
104
98
  # Base set of fields that should always be excluded
105
99
  BASE_EXCLUDE_FIELDS = {PARAM_METADATA}
106
100
 
@@ -15,6 +15,7 @@ from mcp_agent.llm.providers.augmented_llm_generic import GenericAugmentedLLM
15
15
  from mcp_agent.llm.providers.augmented_llm_google import GoogleAugmentedLLM
16
16
  from mcp_agent.llm.providers.augmented_llm_openai import OpenAIAugmentedLLM
17
17
  from mcp_agent.llm.providers.augmented_llm_openrouter import OpenRouterAugmentedLLM
18
+ from mcp_agent.llm.providers.augmented_llm_tensorzero import TensorZeroAugmentedLLM
18
19
  from mcp_agent.mcp.interfaces import AugmentedLLMProtocol
19
20
 
20
21
  # from mcp_agent.workflows.llm.augmented_llm_deepseek import DeekSeekAugmentedLLM
@@ -28,6 +29,7 @@ LLMClass = Union[
28
29
  Type[PlaybackLLM],
29
30
  Type[DeepSeekAugmentedLLM],
30
31
  Type[OpenRouterAugmentedLLM],
32
+ Type[TensorZeroAugmentedLLM],
31
33
  ]
32
34
 
33
35
 
@@ -110,6 +112,7 @@ class ModelFactory:
110
112
  Provider.GENERIC: GenericAugmentedLLM,
111
113
  Provider.GOOGLE: GoogleAugmentedLLM, # type: ignore
112
114
  Provider.OPENROUTER: OpenRouterAugmentedLLM,
115
+ Provider.TENSORZERO: TensorZeroAugmentedLLM,
113
116
  }
114
117
 
115
118
  # Mapping of special model names to their specific LLM classes
@@ -142,6 +145,11 @@ class ModelFactory:
142
145
  provider = Provider(potential_provider)
143
146
  model_parts = model_parts[1:]
144
147
 
148
+ if provider == Provider.TENSORZERO and not model_parts:
149
+ raise ModelConfigError(
150
+ f"TensorZero provider requires a function name after the provider "
151
+ f"(e.g., tensorzero.my-function), got: {model_string}"
152
+ )
145
153
  # Join remaining parts as model name
146
154
  model_name = ".".join(model_parts)
147
155
 
@@ -15,3 +15,4 @@ class Provider(Enum):
15
15
  DEEPSEEK = "deepseek"
16
16
  GENERIC = "generic"
17
17
  OPENROUTER = "openrouter"
18
+ TENSORZERO = "tensorzero" # For TensorZero Gateway
@@ -62,6 +62,7 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
62
62
  AugmentedLLM.PARAM_USE_HISTORY,
63
63
  AugmentedLLM.PARAM_MAX_ITERATIONS,
64
64
  AugmentedLLM.PARAM_PARALLEL_TOOL_CALLS,
65
+ AugmentedLLM.PARAM_TEMPLATE_VARS,
65
66
  }
66
67
 
67
68
  def __init__(self, *args, **kwargs) -> None:
@@ -56,6 +56,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
56
56
  AugmentedLLM.PARAM_PARALLEL_TOOL_CALLS,
57
57
  AugmentedLLM.PARAM_USE_HISTORY,
58
58
  AugmentedLLM.PARAM_MAX_ITERATIONS,
59
+ AugmentedLLM.PARAM_TEMPLATE_VARS,
59
60
  }
60
61
 
61
62
  def __init__(self, provider: Provider = Provider.OPENAI, *args, **kwargs) -> None:
@@ -143,7 +144,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
143
144
  function={
144
145
  "name": tool.name,
145
146
  "description": tool.description if tool.description else "",
146
- "parameters": tool.inputSchema,
147
+ "parameters": self.adjust_schema(tool.inputSchema),
147
148
  },
148
149
  )
149
150
  for tool in response.tools
@@ -350,3 +351,15 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
350
351
  base_args, request_params, self.OPENAI_EXCLUDE_FIELDS.union(self.BASE_EXCLUDE_FIELDS)
351
352
  )
352
353
  return arguments
354
+
355
+ def adjust_schema(self, inputSchema: Dict) -> Dict:
356
+ # return inputSchema
357
+ if not Provider.OPENAI == self.provider:
358
+ return inputSchema
359
+
360
+ if "properties" in inputSchema:
361
+ return inputSchema
362
+
363
+ result = inputSchema.copy()
364
+ result["properties"] = {}
365
+ return result
@@ -0,0 +1,442 @@
1
+ import json
2
+ from typing import Any, Dict, List, Optional, Tuple, Union
3
+
4
+ from mcp.types import (
5
+ CallToolRequest,
6
+ CallToolRequestParams,
7
+ CallToolResult,
8
+ EmbeddedResource,
9
+ ImageContent,
10
+ TextContent,
11
+ )
12
+ from tensorzero import AsyncTensorZeroGateway
13
+ from tensorzero.types import (
14
+ ChatInferenceResponse,
15
+ JsonInferenceResponse,
16
+ TensorZeroError,
17
+ )
18
+
19
+ from mcp_agent.agents.agent import Agent
20
+ from mcp_agent.core.exceptions import ModelConfigError
21
+ from mcp_agent.core.request_params import RequestParams
22
+ from mcp_agent.llm.augmented_llm import AugmentedLLM
23
+ from mcp_agent.llm.memory import Memory, SimpleMemory
24
+ from mcp_agent.llm.provider_types import Provider
25
+ from mcp_agent.llm.providers.multipart_converter_tensorzero import TensorZeroConverter
26
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
27
+
28
+
29
+ class TensorZeroAugmentedLLM(AugmentedLLM[Dict[str, Any], Any]):
30
+ """
31
+ AugmentedLLM implementation for TensorZero using its native API.
32
+ Uses the Converter pattern for message formatting.
33
+ Implements multi-turn tool calling logic, storing API dicts in history.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ agent: Agent,
39
+ model: str,
40
+ request_params: Optional[RequestParams] = None,
41
+ **kwargs: Any,
42
+ ):
43
+ self._t0_gateway: Optional[AsyncTensorZeroGateway] = None
44
+ self._t0_function_name: str = model
45
+ self._t0_episode_id: Optional[str] = kwargs.get("episode_id")
46
+
47
+ super().__init__(
48
+ agent=agent,
49
+ model=model,
50
+ provider=Provider.TENSORZERO,
51
+ request_params=request_params,
52
+ **kwargs,
53
+ )
54
+
55
+ self.history: Memory[Dict[str, Any]] = SimpleMemory[Dict[str, Any]]()
56
+
57
+ self.logger.info(
58
+ f"TensorZero LLM provider initialized for function '{self._t0_function_name}'. History type: {type(self.history)}"
59
+ )
60
+
61
+ @staticmethod
62
+ def block_to_dict(block: Any) -> Dict[str, Any]:
63
+ if hasattr(block, "model_dump"):
64
+ try:
65
+ dumped = block.model_dump(mode="json")
66
+ if dumped:
67
+ return dumped
68
+ except Exception:
69
+ pass
70
+ if hasattr(block, "__dict__"):
71
+ try:
72
+ block_vars = vars(block)
73
+ if block_vars:
74
+ return block_vars
75
+ except Exception:
76
+ pass
77
+ if isinstance(block, (str, int, float, bool, list, dict, type(None))):
78
+ return {"type": "raw", "content": block}
79
+
80
+ # Basic attribute extraction as fallback
81
+ d = {"type": getattr(block, "type", "unknown")}
82
+ for attr in ["id", "name", "text", "arguments"]:
83
+ if hasattr(block, attr):
84
+ d[attr] = getattr(block, attr)
85
+ if len(d) == 1 and d.get("type") == "unknown":
86
+ d["content"] = str(block)
87
+ return d
88
+
89
+ def _initialize_default_params(self, kwargs: dict) -> RequestParams:
90
+ func_name = kwargs.get("model", self._t0_function_name or "unknown_t0_function")
91
+ return RequestParams(
92
+ model=func_name,
93
+ systemPrompt=self.instruction,
94
+ maxTokens=4096,
95
+ use_history=True,
96
+ max_iterations=10, # Max iterations for tool use loop
97
+ parallel_tool_calls=True,
98
+ )
99
+
100
+ async def _initialize_gateway(self) -> AsyncTensorZeroGateway:
101
+ if self._t0_gateway is None:
102
+ self.logger.debug("Initializing AsyncTensorZeroGateway client...")
103
+ try:
104
+ base_url: Optional[str] = None
105
+ default_url = "http://localhost:3000"
106
+
107
+ if (
108
+ self.context
109
+ and self.context.config
110
+ and hasattr(self.context.config, "tensorzero")
111
+ and self.context.config.tensorzero
112
+ ):
113
+ base_url = getattr(self.context.config.tensorzero, "base_url", None)
114
+
115
+ if not base_url:
116
+ if not self.context:
117
+ # Handle case where context itself is missing, log and use default
118
+ self.logger.warning(
119
+ f"LLM context not found. Cannot read TensorZero Gateway base URL configuration. "
120
+ f"Using default: {default_url}"
121
+ )
122
+ else:
123
+ self.logger.warning(
124
+ f"TensorZero Gateway base URL not configured in context.config.tensorzero.base_url. "
125
+ f"Using default: {default_url}"
126
+ )
127
+
128
+ base_url = default_url
129
+
130
+ self._t0_gateway = await AsyncTensorZeroGateway.build_http(gateway_url=base_url) # type: ignore
131
+ self.logger.info(f"TensorZero Gateway client initialized for URL: {base_url}")
132
+ except Exception as e:
133
+ self.logger.error(f"Failed to initialize TensorZero Gateway: {e}")
134
+ raise ModelConfigError(f"Failed to initialize TensorZero Gateway lazily: {e}")
135
+
136
+ return self._t0_gateway
137
+
138
+ async def _apply_prompt_provider_specific(
139
+ self,
140
+ multipart_messages: List[PromptMessageMultipart],
141
+ request_params: Optional[RequestParams] = None,
142
+ is_template: bool = False,
143
+ ) -> PromptMessageMultipart:
144
+ gateway = await self._initialize_gateway()
145
+ merged_params = self.get_request_params(request_params)
146
+
147
+ # [1] Retrieve history
148
+ current_api_messages: List[Dict[str, Any]] = []
149
+ if merged_params.use_history:
150
+ try:
151
+ current_api_messages = self.history.get() or []
152
+ self.logger.debug(
153
+ f"Retrieved {len(current_api_messages)} API dict messages from history."
154
+ )
155
+ except Exception as e:
156
+ self.logger.error(f"Error retrieving history: {e}")
157
+
158
+ # [2] Convert *new* incoming PromptMessageMultipart messages to API dicts
159
+ for msg in multipart_messages:
160
+ msg_dict = TensorZeroConverter.convert_mcp_to_t0_message(msg)
161
+ if msg_dict:
162
+ current_api_messages.append(msg_dict)
163
+
164
+ t0_system_vars = self._prepare_t0_system_params(merged_params)
165
+ if t0_system_vars:
166
+ t0_api_input_dict = {"system": t0_system_vars}
167
+ else:
168
+ t0_api_input_dict = {}
169
+ available_tools: Optional[List[Dict[str, Any]]] = await self._prepare_t0_tools()
170
+
171
+ # [3] Initialize storage arrays for the text content of the assistant message reply and, optionally, tool calls and results, and begin inference loop
172
+ final_assistant_message: List[Union[TextContent, ImageContent, EmbeddedResource]] = []
173
+ last_executed_results: Optional[List[CallToolResult]] = None
174
+
175
+ for i in range(merged_params.max_iterations):
176
+ use_parallel_calls = merged_params.parallel_tool_calls if available_tools else False
177
+ current_t0_episode_id = self._t0_episode_id
178
+
179
+ try:
180
+ self.logger.debug(
181
+ f"Calling TensorZero inference (Iteration {i + 1}/{merged_params.max_iterations})..."
182
+ )
183
+ t0_api_input_dict["messages"] = current_api_messages # type: ignore
184
+
185
+ # [4] Call the TensorZero inference API
186
+ response_iter_or_completion = await gateway.inference(
187
+ function_name=self._t0_function_name,
188
+ input=t0_api_input_dict,
189
+ additional_tools=available_tools,
190
+ parallel_tool_calls=use_parallel_calls,
191
+ stream=False,
192
+ episode_id=current_t0_episode_id,
193
+ )
194
+
195
+ if not isinstance(
196
+ response_iter_or_completion, (ChatInferenceResponse, JsonInferenceResponse)
197
+ ):
198
+ self.logger.error(
199
+ f"Unexpected TensorZero response type: {type(response_iter_or_completion)}"
200
+ )
201
+ final_assistant_message = [
202
+ TextContent(type="text", text="Unexpected response type")
203
+ ]
204
+ break # Exit loop
205
+
206
+ # [5] quick check to confirm that episode_id is present and being used correctly by TensorZero
207
+ completion = response_iter_or_completion
208
+ if completion.episode_id: #
209
+ self._t0_episode_id = str(completion.episode_id)
210
+ if (
211
+ self._t0_episode_id != current_t0_episode_id
212
+ and current_t0_episode_id is not None
213
+ ):
214
+ raise Exception(
215
+ f"Episode ID mismatch: {self._t0_episode_id} != {current_t0_episode_id}"
216
+ )
217
+
218
+ # [6] Adapt TensorZero inference response to a format compatible with the broader framework
219
+ (
220
+ content_parts_this_turn, # Text/Image content ONLY
221
+ executed_results_this_iter, # Results from THIS iteration
222
+ raw_tool_call_blocks,
223
+ ) = await self._adapt_t0_native_completion(completion, available_tools)
224
+
225
+ last_executed_results = (
226
+ executed_results_this_iter # Track results from this iteration
227
+ )
228
+
229
+ # [7] If a text message was returned from the assistant, format that message using the multipart_converter_tensorzero.py helper methods and add this to the current list of API messages
230
+ assistant_api_content = []
231
+ for part in content_parts_this_turn:
232
+ api_part = TensorZeroConverter._convert_content_part(part)
233
+ if api_part:
234
+ assistant_api_content.append(api_part)
235
+ if raw_tool_call_blocks:
236
+ assistant_api_content.extend(
237
+ [self.block_to_dict(b) for b in raw_tool_call_blocks]
238
+ )
239
+
240
+ if assistant_api_content:
241
+ assistant_api_message_dict = {
242
+ "role": "assistant",
243
+ "content": assistant_api_content,
244
+ }
245
+ current_api_messages.append(assistant_api_message_dict)
246
+ elif executed_results_this_iter:
247
+ self.logger.debug(
248
+ "Assistant turn contained only tool calls, no API message added."
249
+ )
250
+
251
+ final_assistant_message = content_parts_this_turn
252
+
253
+ # [8] If there were no tool calls we're done. If not, format the tool results and add them to the current list of API messages
254
+ if not executed_results_this_iter:
255
+ self.logger.debug(f"Iteration {i + 1}: No tool calls detected. Finishing loop.")
256
+ break
257
+ else:
258
+ user_message_with_results = (
259
+ TensorZeroConverter.convert_tool_results_to_t0_user_message(
260
+ executed_results_this_iter
261
+ )
262
+ )
263
+ if user_message_with_results:
264
+ current_api_messages.append(user_message_with_results)
265
+ else:
266
+ self.logger.error("Converter failed to format tool results, breaking loop.")
267
+ break
268
+
269
+ # Check max iterations: TODO: implement logic in the future to handle this dynamically, checking for the presence of a tool call in the last iteration
270
+ if i == merged_params.max_iterations - 1:
271
+ self.logger.warning(f"Max iterations ({merged_params.max_iterations}) reached.")
272
+ break
273
+
274
+ # --- Error Handling for Inference Call ---
275
+ except TensorZeroError as e:
276
+ error_details = getattr(e, "detail", str(e.args[0] if e.args else e))
277
+ self.logger.error(f"TensorZero Error (HTTP {e.status_code}): {error_details}")
278
+ error_content = TextContent(type="text", text=f"TensorZero Error: {error_details}")
279
+ return PromptMessageMultipart(role="assistant", content=[error_content])
280
+ except Exception as e:
281
+ import traceback
282
+
283
+ self.logger.error(f"Unexpected Error: {e}\n{traceback.format_exc()}")
284
+ error_content = TextContent(type="text", text=f"Unexpected error: {e}")
285
+ return PromptMessageMultipart(role="assistant", content=[error_content])
286
+
287
+ # [9] Construct the final assistant message and update history
288
+ final_message_to_return = PromptMessageMultipart(
289
+ role="assistant", content=final_assistant_message
290
+ )
291
+
292
+ if merged_params.use_history:
293
+ try:
294
+ # Store the final list of API DICTIONARIES in history
295
+ self.history.set(current_api_messages)
296
+ self.logger.debug(
297
+ f"Updated self.history with {len(current_api_messages)} API message dicts."
298
+ )
299
+ except Exception as e:
300
+ self.logger.error(f"Failed to update self.history after loop: {e}")
301
+
302
+ # [10] Post final assistant message to display
303
+ display_text = final_message_to_return.all_text()
304
+ if display_text and display_text != "<no text>":
305
+ title = f"ASSISTANT/{self._t0_function_name}"
306
+ await self.show_assistant_message(message_text=display_text, title=title)
307
+
308
+ elif not final_assistant_message and last_executed_results:
309
+ self.logger.debug("Final assistant turn involved only tool calls, no text to display.")
310
+
311
+ return final_message_to_return
312
+
313
+ def _prepare_t0_system_params(self, merged_params: RequestParams) -> Dict[str, Any]:
314
+ """Prepares the 'system' dictionary part of the main input."""
315
+ t0_func_input = merged_params.template_vars.copy()
316
+
317
+ metadata_args = None
318
+ if merged_params.metadata and isinstance(merged_params.metadata, dict):
319
+ metadata_args = merged_params.metadata.get("tensorzero_arguments")
320
+ if isinstance(metadata_args, dict):
321
+ t0_func_input.update(metadata_args)
322
+ self.logger.debug(f"Merged tensorzero_arguments from metadata: {metadata_args}")
323
+ return t0_func_input
324
+
325
+ async def _prepare_t0_tools(self) -> Optional[List[Dict[str, Any]]]:
326
+ """Fetches and formats tools for the additional_tools parameter."""
327
+ formatted_tools: List[Dict[str, Any]] = []
328
+ try:
329
+ tools_response = await self.aggregator.list_tools()
330
+ if tools_response and hasattr(tools_response, "tools") and tools_response.tools:
331
+ for mcp_tool in tools_response.tools:
332
+ if (
333
+ not isinstance(mcp_tool.inputSchema, dict)
334
+ or mcp_tool.inputSchema.get("type") != "object"
335
+ ):
336
+ self.logger.warning(
337
+ f"Tool '{mcp_tool.name}' has invalid parameters schema. Skipping."
338
+ )
339
+ continue
340
+ t0_tool_dict = {
341
+ "name": mcp_tool.name,
342
+ "description": mcp_tool.description if mcp_tool.description else "",
343
+ "parameters": mcp_tool.inputSchema,
344
+ }
345
+ formatted_tools.append(t0_tool_dict)
346
+ return formatted_tools if formatted_tools else None
347
+ except Exception as e:
348
+ self.logger.error(f"Failed to fetch or format tools: {e}")
349
+ return None
350
+
351
+ async def _adapt_t0_native_completion(
352
+ self,
353
+ completion: Union[ChatInferenceResponse, JsonInferenceResponse],
354
+ available_tools_for_display: Optional[List[Dict[str, Any]]] = None,
355
+ ) -> Tuple[
356
+ List[Union[TextContent, ImageContent, EmbeddedResource]], # Text/Image content ONLY
357
+ List[CallToolResult], # Executed results
358
+ List[Any], # Raw tool_call blocks
359
+ ]:
360
+ content_parts_this_turn: List[Union[TextContent, ImageContent, EmbeddedResource]] = []
361
+ executed_tool_results: List[CallToolResult] = []
362
+ raw_tool_call_blocks_from_t0: List[Any] = []
363
+
364
+ if isinstance(completion, ChatInferenceResponse) and hasattr(completion, "content"):
365
+ for block in completion.content:
366
+ block_type = getattr(block, "type", "UNKNOWN")
367
+
368
+ if block_type == "text":
369
+ text_val = getattr(block, "text", None)
370
+ if text_val is not None:
371
+ content_parts_this_turn.append(TextContent(type="text", text=text_val))
372
+
373
+ elif block_type == "tool_call":
374
+ raw_tool_call_blocks_from_t0.append(block)
375
+ tool_use_id = getattr(block, "id", None)
376
+ tool_name = getattr(block, "name", None)
377
+ tool_input_raw = getattr(block, "arguments", None)
378
+ tool_input = {}
379
+ if isinstance(tool_input_raw, dict):
380
+ tool_input = tool_input_raw
381
+ elif isinstance(tool_input_raw, str):
382
+ try:
383
+ tool_input = json.loads(tool_input_raw)
384
+ except json.JSONDecodeError:
385
+ tool_input = {}
386
+ elif tool_input_raw is not None:
387
+ tool_input = {}
388
+
389
+ if tool_use_id and tool_name:
390
+ self.show_tool_call(
391
+ available_tools_for_display, tool_name, json.dumps(tool_input)
392
+ )
393
+ mcp_tool_request = CallToolRequest(
394
+ method="tools/call",
395
+ params=CallToolRequestParams(name=tool_name, arguments=tool_input),
396
+ )
397
+ try:
398
+ result: CallToolResult = await self.call_tool(
399
+ mcp_tool_request, tool_use_id
400
+ )
401
+ setattr(result, "_t0_tool_use_id_temp", tool_use_id)
402
+ setattr(result, "_t0_tool_name_temp", tool_name)
403
+ setattr(result, "_t0_is_error_temp", False)
404
+ executed_tool_results.append(result)
405
+ self.show_oai_tool_result(str(result))
406
+ except Exception as e:
407
+ self.logger.error(
408
+ f"Error executing tool {tool_name} (id: {tool_use_id}): {e}"
409
+ )
410
+ error_text = f"Error executing tool {tool_name}: {str(e)}"
411
+ error_result = CallToolResult(
412
+ isError=True, content=[TextContent(type="text", text=error_text)]
413
+ )
414
+ setattr(error_result, "_t0_tool_use_id_temp", tool_use_id)
415
+ setattr(error_result, "_t0_tool_name_temp", tool_name)
416
+ setattr(error_result, "_t0_is_error_temp", True)
417
+ executed_tool_results.append(error_result)
418
+ self.show_oai_tool_result(f"ERROR: {error_text}")
419
+
420
+ elif block_type == "thought":
421
+ thought_text = getattr(block, "text", None)
422
+ self.logger.debug(f"TensorZero thought: {thought_text}")
423
+ else:
424
+ self.logger.warning(
425
+ f"TensorZero Adapt: Skipping unknown block type: {block_type}"
426
+ )
427
+
428
+ elif isinstance(completion, JsonInferenceResponse):
429
+ # `completion.output.raw` should always be present unless the LLM provider returns unexpected data
430
+ if completion.output.raw:
431
+ content_parts_this_turn.append(TextContent(type="text", text=completion.output.raw))
432
+
433
+ return content_parts_this_turn, executed_tool_results, raw_tool_call_blocks_from_t0
434
+
435
+ async def shutdown(self):
436
+ """Close the TensorZero gateway client if initialized."""
437
+ if self._t0_gateway:
438
+ try:
439
+ await self._t0_gateway.close()
440
+ self.logger.debug("TensorZero Gateway client closed.")
441
+ except Exception as e:
442
+ self.logger.error(f"Error closing TensorZero Gateway client: {e}")
@@ -0,0 +1,200 @@
1
+ import json
2
+ from typing import Any, Dict, List, Optional, Union
3
+
4
+ from mcp.types import (
5
+ CallToolResult,
6
+ EmbeddedResource,
7
+ ImageContent,
8
+ TextContent,
9
+ )
10
+
11
+ from mcp_agent.logging.logger import get_logger
12
+ from mcp_agent.mcp.helpers.content_helpers import (
13
+ get_text,
14
+ )
15
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
16
+
17
+ _logger = get_logger(__name__)
18
+
19
+
20
+ class TensorZeroConverter:
21
+ """Converts MCP message types to/from TensorZero API format."""
22
+
23
+ @staticmethod
24
+ def _convert_content_part(
25
+ part: Union[TextContent, ImageContent, EmbeddedResource],
26
+ ) -> Optional[Dict[str, Any]]:
27
+ """Converts a single MCP content part to a T0 content block dictionary."""
28
+ if isinstance(part, TextContent):
29
+ text = get_text(part)
30
+ if text is not None:
31
+ return {"type": "text", "text": text}
32
+ elif isinstance(part, ImageContent):
33
+ # Handle Base64: needs data, mimeType (and mimeType must not be empty)
34
+ if hasattr(part, "data") and part.data and hasattr(part, "mimeType") and part.mimeType:
35
+ _logger.debug(
36
+ f"Converting ImageContent as base64 for T0 native: mime={part.mimeType}, data_len={len(part.data) if isinstance(part.data, str) else 'N/A'}"
37
+ )
38
+ supported_mime_types = ["image/jpeg", "image/png", "image/webp"]
39
+ mime_type = getattr(part, "mimeType", "")
40
+
41
+ # Use the provided mime_type if supported, otherwise default to png
42
+ if mime_type not in supported_mime_types:
43
+ _logger.warning(
44
+ f"Unsupported mimeType '{mime_type}' for T0 base64 image, defaulting to image/png."
45
+ )
46
+ mime_type = "image/png"
47
+
48
+ return {
49
+ "type": "image",
50
+ "mime_type": mime_type, # Note: T0 uses mime_type, not media_type
51
+ "data": getattr(part, "data", ""), # Data is direct property
52
+ }
53
+ else:
54
+ # Log cases where it's an ImageContent but doesn't fit Base64 criteria
55
+ _logger.warning(
56
+ f"Skipping ImageContent: Missing required base64 fields (mimeType/data), or mimeType is empty: {part}"
57
+ )
58
+
59
+ elif isinstance(part, EmbeddedResource):
60
+ _logger.warning(f"Skipping EmbeddedResource, T0 conversion not implemented: {part}")
61
+ else:
62
+ _logger.error(
63
+ f"Unsupported content part type for T0 conversion: {type(part)}"
64
+ ) # Changed to error
65
+
66
+ return None # Return None if no block was successfully created
67
+
68
+ @staticmethod
69
+ def _get_text_from_call_tool_result(result: CallToolResult) -> str:
70
+ """Helper to extract combined text from a CallToolResult's content list."""
71
+ texts = []
72
+ if result.content:
73
+ for part in result.content:
74
+ text = get_text(part)
75
+ if text:
76
+ texts.append(text)
77
+ return "\n".join(texts)
78
+
79
+ @staticmethod
80
+ def convert_tool_results_to_t0_user_message(
81
+ results: List[CallToolResult],
82
+ ) -> Optional[Dict[str, Any]]:
83
+ """Formats CallToolResult list into T0's tool_result blocks within a user message dict."""
84
+ t0_tool_result_blocks = []
85
+ for result in results:
86
+ tool_use_id = getattr(result, "_t0_tool_use_id_temp", None)
87
+ tool_name = getattr(result, "_t0_tool_name_temp", None)
88
+
89
+ if tool_use_id and tool_name:
90
+ result_content_str = TensorZeroConverter._get_text_from_call_tool_result(result)
91
+ try:
92
+ # Attempt to treat result as JSON if possible, else use raw string
93
+ try:
94
+ json_result = json.loads(result_content_str)
95
+ except json.JSONDecodeError:
96
+ json_result = result_content_str # Fallback to string if not valid JSON
97
+ except Exception as e:
98
+ _logger.error(f"Unexpected error processing tool result content: {e}")
99
+ json_result = str(result_content_str) # Safest fallback
100
+
101
+ t0_block = {
102
+ "type": "tool_result",
103
+ "id": tool_use_id,
104
+ "name": tool_name,
105
+ "result": json_result, # T0 expects the result directly
106
+ }
107
+ t0_tool_result_blocks.append(t0_block)
108
+
109
+ # Clean up temporary attributes
110
+ try:
111
+ delattr(result, "_t0_tool_use_id_temp")
112
+ delattr(result, "_t0_tool_name_temp")
113
+ if hasattr(result, "_t0_is_error_temp"):
114
+ delattr(result, "_t0_is_error_temp")
115
+ except AttributeError:
116
+ pass
117
+ else:
118
+ _logger.warning(
119
+ f"Could not find id/name temp attributes for CallToolResult: {result}"
120
+ )
121
+
122
+ if not t0_tool_result_blocks:
123
+ return None
124
+
125
+ return {"role": "user", "content": t0_tool_result_blocks}
126
+
127
+ @staticmethod
128
+ def convert_mcp_to_t0_message(msg: PromptMessageMultipart) -> Optional[Dict[str, Any]]:
129
+ """
130
+ Converts a single PromptMessageMultipart to a T0 API message dictionary.
131
+ Handles Text, Image, and embedded CallToolResult content.
132
+ Skips system messages.
133
+ """
134
+ if msg.role == "system":
135
+ return None
136
+
137
+ t0_content_blocks = []
138
+ contains_tool_result = False
139
+
140
+ for part in msg.content:
141
+ # Use the corrected _convert_content_part
142
+ converted_block = TensorZeroConverter._convert_content_part(part)
143
+ if converted_block:
144
+ t0_content_blocks.append(converted_block)
145
+ elif isinstance(part, CallToolResult):
146
+ # Existing logic for handling embedded CallToolResult (seems compatible with T0 tool_result spec)
147
+ contains_tool_result = True
148
+ tool_use_id = getattr(part, "_t0_tool_use_id_temp", None)
149
+ tool_name = getattr(part, "_t0_tool_name_temp", None)
150
+ if tool_use_id and tool_name:
151
+ result_content_str = TensorZeroConverter._get_text_from_call_tool_result(part)
152
+ # Try to format result as JSON object/string
153
+ try:
154
+ json_result = json.loads(result_content_str)
155
+ except json.JSONDecodeError:
156
+ json_result = result_content_str # Fallback
157
+ except Exception as e:
158
+ _logger.error(f"Error processing embedded tool result: {e}")
159
+ json_result = str(result_content_str)
160
+
161
+ t0_content_blocks.append(
162
+ {
163
+ "type": "tool_result",
164
+ "id": tool_use_id,
165
+ "name": tool_name,
166
+ "result": json_result,
167
+ }
168
+ )
169
+ # Clean up temp attributes
170
+ try:
171
+ delattr(part, "_t0_tool_use_id_temp")
172
+ delattr(part, "_t0_tool_name_temp")
173
+ except AttributeError:
174
+ pass
175
+ else:
176
+ _logger.warning(
177
+ f"Found embedded CallToolResult without required temp attributes: {part}"
178
+ )
179
+ # Note: The _convert_content_part handles logging for other skipped/unsupported types
180
+
181
+ if not t0_content_blocks:
182
+ return None
183
+
184
+ # Determine role - logic remains the same
185
+ valid_role = msg.role if msg.role in ["user", "assistant"] else "user"
186
+ if contains_tool_result and all(
187
+ block.get("type") == "tool_result" for block in t0_content_blocks
188
+ ):
189
+ final_role = "user"
190
+ if valid_role != final_role:
191
+ _logger.debug(f"Overriding role to '{final_role}' for tool result message.")
192
+ else:
193
+ final_role = valid_role
194
+ if valid_role != msg.role:
195
+ _logger.warning(f"Mapping message role '{msg.role}' to '{valid_role}' for T0.")
196
+
197
+ return {"role": final_role, "content": t0_content_blocks}
198
+
199
+ # Add methods here if needed to convert *from* T0 format back to MCP types
200
+ # e.g., adapt_t0_response_to_mcp(...) - this logic stays in the LLM class for now
@@ -15,6 +15,7 @@ from typing import (
15
15
 
16
16
  from anyio import Event, Lock, create_task_group
17
17
  from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
18
+ from httpx import HTTPStatusError
18
19
  from mcp import ClientSession
19
20
  from mcp.client.sse import sse_client
20
21
  from mcp.client.stdio import (
@@ -162,14 +163,26 @@ async def _server_lifecycle_task(server_conn: ServerConnection) -> None:
162
163
  transport_context = server_conn._transport_context_factory()
163
164
 
164
165
  async with transport_context as (read_stream, write_stream):
165
- # try:
166
166
  server_conn.create_session(read_stream, write_stream)
167
167
 
168
168
  async with server_conn.session:
169
169
  await server_conn.initialize_session()
170
-
171
170
  await server_conn.wait_for_shutdown_request()
172
171
 
172
+ except HTTPStatusError as http_exc:
173
+ logger.error(
174
+ f"{server_name}: Lifecycle task encountered HTTP error: {http_exc}",
175
+ exc_info=True,
176
+ data={
177
+ "progress_action": ProgressAction.FATAL_ERROR,
178
+ "server_name": server_name,
179
+ },
180
+ )
181
+ server_conn._error_occurred = True
182
+ server_conn._error_message = f"HTTP Error: {http_exc.response.status_code} {http_exc.response.reason_phrase} for URL: {http_exc.request.url}"
183
+ server_conn._initialized_event.set()
184
+ # No raise - let get_server handle it with a friendly message
185
+
173
186
  except Exception as exc:
174
187
  logger.error(
175
188
  f"{server_name}: Lifecycle task encountered an error: {exc}",
@@ -180,7 +193,27 @@ async def _server_lifecycle_task(server_conn: ServerConnection) -> None:
180
193
  },
181
194
  )
182
195
  server_conn._error_occurred = True
183
- server_conn._error_message = traceback.format_exception(exc)
196
+
197
+ if "ExceptionGroup" in type(exc).__name__ and hasattr(exc, "exceptions"):
198
+ # Handle ExceptionGroup better by extracting the actual errors
199
+ error_messages = []
200
+ for subexc in exc.exceptions:
201
+ if isinstance(subexc, HTTPStatusError):
202
+ # Special handling for HTTP errors to make them more user-friendly
203
+ error_messages.append(
204
+ f"HTTP Error: {subexc.response.status_code} {subexc.response.reason_phrase} for URL: {subexc.request.url}"
205
+ )
206
+ else:
207
+ error_messages.append(f"Error: {type(subexc).__name__}: {subexc}")
208
+ if hasattr(subexc, "__cause__") and subexc.__cause__:
209
+ error_messages.append(
210
+ f"Caused by: {type(subexc.__cause__).__name__}: {subexc.__cause__}"
211
+ )
212
+ server_conn._error_message = error_messages
213
+ else:
214
+ # For regular exceptions, keep the traceback but format it more cleanly
215
+ server_conn._error_message = traceback.format_exception(exc)
216
+
184
217
  # If there's an error, we should also set the event so that
185
218
  # 'get_server' won't hang
186
219
  server_conn._initialized_event.set()
@@ -277,6 +310,7 @@ class MCPConnectionManager(ContextDependent):
277
310
  config.headers,
278
311
  sse_read_timeout=config.read_transport_sse_timeout_seconds,
279
312
  )
313
+
280
314
  else:
281
315
  raise ValueError(f"Unsupported transport: {config.transport}")
282
316
 
@@ -333,9 +367,17 @@ class MCPConnectionManager(ContextDependent):
333
367
  # Check if the server is healthy after initialization
334
368
  if not server_conn.is_healthy():
335
369
  error_msg = server_conn._error_message or "Unknown error"
370
+
371
+ # Format the error message for better display
372
+ if isinstance(error_msg, list):
373
+ # Join the list with newlines for better readability
374
+ formatted_error = "\n".join(error_msg)
375
+ else:
376
+ formatted_error = str(error_msg)
377
+
336
378
  raise ServerInitializationError(
337
379
  f"MCP Server: '{server_name}': Failed to initialize - see details. Check fastagent.config.yaml?",
338
- error_msg,
380
+ formatted_error,
339
381
  )
340
382
 
341
383
  return server_conn