fast-agent-mcp 0.2.40__py3-none-any.whl → 0.2.42__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (45) hide show
  1. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.42.dist-info}/METADATA +2 -1
  2. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.42.dist-info}/RECORD +45 -40
  3. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.42.dist-info}/entry_points.txt +2 -2
  4. mcp_agent/agents/base_agent.py +111 -1
  5. mcp_agent/cli/__main__.py +29 -3
  6. mcp_agent/cli/commands/check_config.py +140 -81
  7. mcp_agent/cli/commands/go.py +151 -38
  8. mcp_agent/cli/commands/quickstart.py +6 -2
  9. mcp_agent/cli/commands/server_helpers.py +106 -0
  10. mcp_agent/cli/constants.py +25 -0
  11. mcp_agent/cli/main.py +1 -1
  12. mcp_agent/config.py +111 -44
  13. mcp_agent/core/agent_app.py +104 -15
  14. mcp_agent/core/agent_types.py +5 -1
  15. mcp_agent/core/direct_decorators.py +38 -0
  16. mcp_agent/core/direct_factory.py +18 -4
  17. mcp_agent/core/enhanced_prompt.py +173 -13
  18. mcp_agent/core/fastagent.py +4 -0
  19. mcp_agent/core/interactive_prompt.py +37 -37
  20. mcp_agent/core/usage_display.py +11 -1
  21. mcp_agent/core/validation.py +21 -2
  22. mcp_agent/human_input/elicitation_form.py +53 -21
  23. mcp_agent/llm/augmented_llm.py +28 -9
  24. mcp_agent/llm/augmented_llm_silent.py +48 -0
  25. mcp_agent/llm/model_database.py +20 -0
  26. mcp_agent/llm/model_factory.py +21 -0
  27. mcp_agent/llm/provider_key_manager.py +22 -8
  28. mcp_agent/llm/provider_types.py +20 -12
  29. mcp_agent/llm/providers/augmented_llm_anthropic.py +7 -2
  30. mcp_agent/llm/providers/augmented_llm_azure.py +7 -1
  31. mcp_agent/llm/providers/augmented_llm_bedrock.py +1787 -0
  32. mcp_agent/llm/providers/augmented_llm_google_native.py +4 -1
  33. mcp_agent/llm/providers/augmented_llm_openai.py +12 -3
  34. mcp_agent/llm/providers/augmented_llm_xai.py +38 -0
  35. mcp_agent/llm/usage_tracking.py +28 -3
  36. mcp_agent/logging/logger.py +7 -0
  37. mcp_agent/mcp/hf_auth.py +32 -4
  38. mcp_agent/mcp/mcp_agent_client_session.py +2 -0
  39. mcp_agent/mcp/mcp_aggregator.py +38 -44
  40. mcp_agent/mcp/sampling.py +15 -11
  41. mcp_agent/resources/examples/mcp/elicitations/forms_demo.py +0 -6
  42. mcp_agent/resources/examples/workflows/router.py +9 -0
  43. mcp_agent/ui/console_display.py +125 -13
  44. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.42.dist-info}/WHEEL +0 -0
  45. {fast_agent_mcp-0.2.40.dist-info → fast_agent_mcp-0.2.42.dist-info}/licenses/LICENSE +0 -0
@@ -36,6 +36,7 @@ def display_usage_report(
36
36
  total_input = 0
37
37
  total_output = 0
38
38
  total_tokens = 0
39
+ total_tool_calls = 0
39
40
 
40
41
  for agent_name, agent in agents.items():
41
42
  if agent.usage_accumulator:
@@ -45,6 +46,7 @@ def display_usage_report(
45
46
  output_tokens = summary["cumulative_output_tokens"]
46
47
  billing_tokens = summary["cumulative_billing_tokens"]
47
48
  turns = summary["turn_count"]
49
+ tool_calls = summary["cumulative_tool_calls"]
48
50
 
49
51
  # Get context percentage for this agent
50
52
  context_percentage = agent.usage_accumulator.context_usage_percentage
@@ -72,6 +74,7 @@ def display_usage_report(
72
74
  "output": output_tokens,
73
75
  "total": billing_tokens,
74
76
  "turns": turns,
77
+ "tool_calls": tool_calls,
75
78
  "context": context_percentage,
76
79
  }
77
80
  )
@@ -79,6 +82,7 @@ def display_usage_report(
79
82
  total_input += input_tokens
80
83
  total_output += output_tokens
81
84
  total_tokens += billing_tokens
85
+ total_tool_calls += tool_calls
82
86
 
83
87
  if not usage_data:
84
88
  return
@@ -94,7 +98,7 @@ def display_usage_report(
94
98
 
95
99
  # Print header with proper spacing
96
100
  console.print(
97
- f"[dim]{'Agent':<{agent_width}} {'Input':>9} {'Output':>9} {'Total':>9} {'Turns':>6} {'Context%':>9} {'Model':<25}[/dim]"
101
+ f"[dim]{'Agent':<{agent_width}} {'Input':>9} {'Output':>9} {'Total':>9} {'Turns':>6} {'Tools':>6} {'Context%':>9} {'Model':<25}[/dim]"
98
102
  )
99
103
 
100
104
  # Print agent rows - use styling based on subdued_colors flag
@@ -103,6 +107,7 @@ def display_usage_report(
103
107
  output_str = f"{data['output']:,}"
104
108
  total_str = f"{data['total']:,}"
105
109
  turns_str = str(data["turns"])
110
+ tools_str = str(data["tool_calls"])
106
111
  context_str = f"{data['context']:.1f}%" if data["context"] is not None else "-"
107
112
 
108
113
  # Truncate agent name if needed
@@ -118,6 +123,7 @@ def display_usage_report(
118
123
  f"{output_str:>9} "
119
124
  f"[bold]{total_str:>9}[/bold] "
120
125
  f"{turns_str:>6} "
126
+ f"{tools_str:>6} "
121
127
  f"{context_str:>9} "
122
128
  f"{data['model']:<25}[/dim]"
123
129
  )
@@ -129,6 +135,7 @@ def display_usage_report(
129
135
  f"{output_str:>9} "
130
136
  f"[bold]{total_str:>9}[/bold] "
131
137
  f"{turns_str:>6} "
138
+ f"{tools_str:>6} "
132
139
  f"{context_str:>9} "
133
140
  f"[dim]{data['model']:<25}[/dim]"
134
141
  )
@@ -139,6 +146,7 @@ def display_usage_report(
139
146
  total_input_str = f"{total_input:,}"
140
147
  total_output_str = f"{total_output:,}"
141
148
  total_tokens_str = f"{total_tokens:,}"
149
+ total_tools_str = str(total_tool_calls)
142
150
 
143
151
  if subdued_colors:
144
152
  # Original fastagent.py style with dim wrapper on bold
@@ -148,6 +156,7 @@ def display_usage_report(
148
156
  f"{total_output_str:>9} "
149
157
  f"[bold]{total_tokens_str:>9}[/bold] "
150
158
  f"{'':<6} "
159
+ f"{total_tools_str:>6} "
151
160
  f"{'':<9} "
152
161
  f"{'':<25}[/bold dim]"
153
162
  )
@@ -159,6 +168,7 @@ def display_usage_report(
159
168
  f"[bold]{total_output_str:>9}[/bold] "
160
169
  f"[bold]{total_tokens_str:>9}[/bold] "
161
170
  f"{'':<6} "
171
+ f"[bold]{total_tools_str:>6}[/bold] "
162
172
  f"{'':<9} "
163
173
  f"{'':<25}"
164
174
  )
@@ -52,7 +52,7 @@ def validate_workflow_references(agents: Dict[str, Dict[str, Any]]) -> None:
52
52
 
53
53
  for name, agent_data in agents.items():
54
54
  agent_type = agent_data["type"] # This is a string from config
55
-
55
+
56
56
  # Note: Compare string values from config with the Enum's string value
57
57
  if agent_type == AgentType.PARALLEL.value:
58
58
  # Check fan_in exists
@@ -226,7 +226,7 @@ def get_dependencies_groups(
226
226
  # Build the dependency graph
227
227
  for name, agent_data in agents_dict.items():
228
228
  agent_type = agent_data["type"] # This is a string from config
229
-
229
+
230
230
  # Note: Compare string values from config with the Enum's string value
231
231
  if agent_type == AgentType.PARALLEL.value:
232
232
  # Parallel agents depend on their fan-out and fan-in agents
@@ -305,3 +305,22 @@ def get_dependencies_groups(
305
305
  remaining -= current_level
306
306
 
307
307
  return result
308
+
309
+
310
+ def validate_provider_keys_post_creation(active_agents: Dict[str, Any]) -> None:
311
+ """
312
+ Validate that API keys are available for all created agents with LLMs.
313
+
314
+ This runs after agent creation when we have actual agent instances.
315
+
316
+ Args:
317
+ active_agents: Dictionary of created agent instances
318
+
319
+ Raises:
320
+ ProviderKeyError: If any required API key is missing
321
+ """
322
+ for agent_name, agent in active_agents.items():
323
+ llm = getattr(agent, "_llm", None)
324
+ if llm:
325
+ # This throws a ProviderKeyError if the key is not present
326
+ llm._api_key()
@@ -16,7 +16,6 @@ from prompt_toolkit.validation import ValidationError, Validator
16
16
  from prompt_toolkit.widgets import (
17
17
  Button,
18
18
  Checkbox,
19
- Dialog,
20
19
  Frame,
21
20
  Label,
22
21
  RadioList,
@@ -187,13 +186,31 @@ class ElicitationForm:
187
186
  height=len(self.message.split("\n")),
188
187
  )
189
188
 
190
- # Create form fields - removed useless horizontal divider
191
- form_fields = [
192
- fastagent_header, # Fast-agent info
193
- Window(height=1), # Spacing
194
- mcp_header, # MCP server message
195
- Window(height=1), # Spacing
196
- ]
189
+ # Create sticky headers (outside scrollable area)
190
+ sticky_headers = HSplit(
191
+ [
192
+ Window(height=1), # Top padding
193
+ VSplit(
194
+ [
195
+ Window(width=2), # Left padding
196
+ fastagent_header, # Fast-agent info
197
+ Window(width=2), # Right padding
198
+ ]
199
+ ),
200
+ Window(height=1), # Spacing
201
+ VSplit(
202
+ [
203
+ Window(width=2), # Left padding
204
+ mcp_header, # MCP server message
205
+ Window(width=2), # Right padding
206
+ ]
207
+ ),
208
+ Window(height=1), # Spacing
209
+ ]
210
+ )
211
+
212
+ # Create scrollable form fields (without headers)
213
+ form_fields = []
197
214
 
198
215
  for field_name, field_def in self.properties.items():
199
216
  field_widget = self._create_field(field_name, field_def)
@@ -228,18 +245,17 @@ class ElicitationForm:
228
245
  ]
229
246
  )
230
247
 
231
- # Main layout
248
+ # Main scrollable content (form fields and buttons only)
232
249
  form_fields.extend([self.status_line, buttons])
233
- content = HSplit(form_fields)
250
+ scrollable_form_content = HSplit(form_fields)
234
251
 
235
- # Add padding around content using HSplit and VSplit with empty windows
236
- padded_content = HSplit(
252
+ # Add padding around scrollable content
253
+ padded_scrollable_content = HSplit(
237
254
  [
238
- Window(height=1), # Top padding
239
255
  VSplit(
240
256
  [
241
257
  Window(width=2), # Left padding
242
- content,
258
+ scrollable_form_content,
243
259
  Window(width=2), # Right padding
244
260
  ]
245
261
  ),
@@ -247,20 +263,36 @@ class ElicitationForm:
247
263
  ]
248
264
  )
249
265
 
250
- # Wrap content in ScrollablePane to handle oversized forms
266
+ # Wrap only form fields in ScrollablePane (headers stay fixed)
251
267
  scrollable_content = ScrollablePane(
252
- content=padded_content,
268
+ content=padded_scrollable_content,
253
269
  show_scrollbar=False, # Only show when content exceeds available space
254
270
  display_arrows=False, # Only show when content exceeds available space
255
271
  keep_cursor_visible=True,
256
272
  keep_focused_window_visible=True,
257
273
  )
258
274
 
259
- # Dialog - formatted title with better styling and text
260
- dialog = Dialog(
261
- title=FormattedText([("class:title", "Elicitation Request")]),
262
- body=scrollable_content,
263
- with_background=True, # Re-enable background for proper layout
275
+ # Create title bar manually
276
+ title_bar = Window(
277
+ FormattedTextControl(FormattedText([("class:title", "Elicitation Request")])),
278
+ height=1,
279
+ style="class:dialog.title",
280
+ )
281
+
282
+ # Combine title, sticky headers, and scrollable content
283
+ full_content = HSplit(
284
+ [
285
+ title_bar,
286
+ Window(height=1), # Spacing after title
287
+ sticky_headers, # Headers stay fixed at top
288
+ scrollable_content, # Form fields can scroll
289
+ ]
290
+ )
291
+
292
+ # Create dialog frame manually to avoid Dialog's internal scrolling
293
+ dialog = Frame(
294
+ body=full_content,
295
+ style="class:dialog",
264
296
  )
265
297
 
266
298
  # Key bindings
@@ -36,7 +36,7 @@ from mcp_agent.llm.sampling_format_converter import (
36
36
  BasicFormatConverter,
37
37
  ProviderFormatConverter,
38
38
  )
39
- from mcp_agent.llm.usage_tracking import UsageAccumulator
39
+ from mcp_agent.llm.usage_tracking import TurnUsage, UsageAccumulator
40
40
  from mcp_agent.logging.logger import get_logger
41
41
  from mcp_agent.mcp.helpers.content_helpers import get_text
42
42
  from mcp_agent.mcp.interfaces import (
@@ -123,6 +123,7 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
123
123
  ] = BasicFormatConverter,
124
124
  context: Optional["Context"] = None,
125
125
  model: Optional[str] = None,
126
+ api_key: Optional[str] = None,
126
127
  **kwargs: dict[str, Any],
127
128
  ) -> None:
128
129
  """
@@ -158,6 +159,9 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
158
159
  # Initialize the display component
159
160
  self.display = ConsoleDisplay(config=self.context.config)
160
161
 
162
+ # Tool call counter for current turn
163
+ self._current_turn_tool_calls = 0
164
+
161
165
  # Initialize default parameters, passing model info
162
166
  model_kwargs = kwargs.copy()
163
167
  if model:
@@ -173,6 +177,8 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
173
177
  self.type_converter = type_converter
174
178
  self.verb = kwargs.get("verb")
175
179
 
180
+ self._init_api_key = api_key
181
+
176
182
  # Initialize usage tracking
177
183
  self.usage_accumulator = UsageAccumulator()
178
184
 
@@ -440,15 +446,25 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
440
446
 
441
447
  def show_tool_result(self, result: CallToolResult) -> None:
442
448
  """Display a tool result in a formatted panel."""
443
- self.display.show_tool_result(result)
449
+ self.display.show_tool_result(result, name=self.name)
444
450
 
445
451
  def show_oai_tool_result(self, result: str) -> None:
446
452
  """Display a tool result in a formatted panel."""
447
- self.display.show_oai_tool_result(result)
453
+ self.display.show_oai_tool_result(result, name=self.name)
448
454
 
449
455
  def show_tool_call(self, available_tools, tool_name, tool_args) -> None:
450
456
  """Display a tool call in a formatted panel."""
451
- self.display.show_tool_call(available_tools, tool_name, tool_args)
457
+ self._current_turn_tool_calls += 1
458
+ self.display.show_tool_call(available_tools, tool_name, tool_args, name=self.name)
459
+
460
+ def _reset_turn_tool_calls(self) -> None:
461
+ """Reset tool call counter for new turn."""
462
+ self._current_turn_tool_calls = 0
463
+
464
+ def _finalize_turn_usage(self, turn_usage: "TurnUsage") -> None:
465
+ """Set tool call count on TurnUsage and add to accumulator."""
466
+ turn_usage.set_tool_calls(self._current_turn_tool_calls)
467
+ self.usage_accumulator.add_turn(turn_usage)
452
468
 
453
469
  async def show_assistant_message(
454
470
  self,
@@ -556,12 +572,12 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
556
572
 
557
573
  def _update_streaming_progress(self, content: str, model: str, estimated_tokens: int) -> int:
558
574
  """Update streaming progress with token estimation and formatting.
559
-
575
+
560
576
  Args:
561
577
  content: The text content from the streaming event
562
578
  model: The model name
563
579
  estimated_tokens: Current token count to update
564
-
580
+
565
581
  Returns:
566
582
  Updated estimated token count
567
583
  """
@@ -569,10 +585,10 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
569
585
  text_length = len(content)
570
586
  additional_tokens = max(1, text_length // 4)
571
587
  new_total = estimated_tokens + additional_tokens
572
-
588
+
573
589
  # Format token count for display
574
590
  token_str = str(new_total).rjust(5)
575
-
591
+
576
592
  # Emit progress event
577
593
  data = {
578
594
  "progress_action": ProgressAction.STREAMING,
@@ -582,7 +598,7 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
582
598
  "details": token_str.strip(), # Token count goes in details for STREAMING action
583
599
  }
584
600
  self.logger.info("Streaming progress", data=data)
585
-
601
+
586
602
  return new_total
587
603
 
588
604
  def _log_chat_finished(self, model: Optional[str] = None) -> None:
@@ -692,6 +708,9 @@ class AugmentedLLM(ContextDependent, AugmentedLLMProtocol, Generic[MessageParamT
692
708
  return self._message_history
693
709
 
694
710
  def _api_key(self):
711
+ if self._init_api_key:
712
+ return self._init_api_key
713
+
695
714
  from mcp_agent.llm.provider_key_manager import ProviderKeyManager
696
715
 
697
716
  assert self.provider
@@ -0,0 +1,48 @@
1
+ """Silent LLM implementation that suppresses display output while maintaining functionality."""
2
+
3
+ from typing import Any
4
+
5
+ from mcp_agent.llm.augmented_llm_passthrough import PassthroughLLM
6
+ from mcp_agent.llm.provider_types import Provider
7
+ from mcp_agent.llm.usage_tracking import TurnUsage, UsageAccumulator
8
+
9
+
10
+ class ZeroUsageAccumulator(UsageAccumulator):
11
+ """Usage accumulator that always reports zero usage."""
12
+
13
+ def add_turn(self, turn: TurnUsage) -> None:
14
+ """Override to do nothing - no usage accumulation."""
15
+ pass
16
+
17
+
18
+ class SilentLLM(PassthroughLLM):
19
+ """
20
+ A specialized LLM that processes messages like PassthroughLLM but suppresses all display output.
21
+
22
+ This is particularly useful for parallel agent workflows where the fan-in agent
23
+ should aggregate results without polluting the console with intermediate output.
24
+ Token counting is disabled - the model always reports zero usage.
25
+ """
26
+
27
+ def __init__(
28
+ self, provider=Provider.FAST_AGENT, name: str = "Silent", **kwargs: dict[str, Any]
29
+ ) -> None:
30
+ super().__init__(name=name, provider=provider, **kwargs)
31
+ # Override with zero usage accumulator - silent model reports no usage
32
+ self.usage_accumulator = ZeroUsageAccumulator()
33
+
34
+ def show_user_message(self, message: Any, **kwargs) -> None:
35
+ """Override to suppress user message display."""
36
+ pass
37
+
38
+ async def show_assistant_message(self, message: Any, **kwargs) -> None:
39
+ """Override to suppress assistant message display."""
40
+ pass
41
+
42
+ def show_tool_calls(self, tool_calls: Any, **kwargs) -> None:
43
+ """Override to suppress tool call display."""
44
+ pass
45
+
46
+ def show_tool_results(self, tool_results: Any, **kwargs) -> None:
47
+ """Override to suppress tool result display."""
48
+ pass
@@ -47,6 +47,7 @@ class ModelDatabase:
47
47
  "video/mp4",
48
48
  ]
49
49
  QWEN_MULTIMODAL = ["text/plain", "image/jpeg", "image/png", "image/webp"]
50
+ XAI_VISION = ["text/plain", "image/jpeg", "image/png", "image/webp"]
50
51
  TEXT_ONLY = ["text/plain"]
51
52
 
52
53
  # Common parameter configurations
@@ -128,10 +129,22 @@ class ModelDatabase:
128
129
  context_window=2097152, max_output_tokens=8192, tokenizes=GOOGLE_MULTIMODAL
129
130
  )
130
131
 
132
+ # FIXME: xAI has not documented the max output tokens for Grok 4. Using Grok 3 as a placeholder. Will need to update when available (if ever)
133
+ GROK_4 = ModelParameters(
134
+ context_window=256000, max_output_tokens=16385, tokenizes=XAI_VISION
135
+ )
136
+
137
+ # Source for Grok 3 max output: https://www.reddit.com/r/grok/comments/1j7209p/exploring_grok_3_beta_output_capacity_a_simple/
138
+ # xAI does not document Grok 3 max output tokens, using the above source as a reference.
139
+ GROK_3 = ModelParameters(
140
+ context_window=131072, max_output_tokens=16385, tokenizes=TEXT_ONLY
141
+ )
142
+
131
143
  # Model configuration database
132
144
  MODELS: Dict[str, ModelParameters] = {
133
145
  # internal models
134
146
  "passthrough": FAST_AGENT_STANDARD,
147
+ "silent": FAST_AGENT_STANDARD,
135
148
  "playback": FAST_AGENT_STANDARD,
136
149
  "slow": FAST_AGENT_STANDARD,
137
150
  # aliyun models
@@ -194,6 +207,13 @@ class ModelDatabase:
194
207
  "gemini-2.5-pro-preview": GEMINI_2_5_PRO,
195
208
  "gemini-2.5-flash-preview-05-20": GEMINI_FLASH,
196
209
  "gemini-2.5-pro-preview-05-06": GEMINI_PRO,
210
+ # xAI Grok Models
211
+ "grok-4": GROK_4,
212
+ "grok-4-0709": GROK_4,
213
+ "grok-3": GROK_3,
214
+ "grok-3-mini": GROK_3,
215
+ "grok-3-fast": GROK_3,
216
+ "grok-3-mini-fast": GROK_3,
197
217
  }
198
218
 
199
219
  @classmethod
@@ -8,11 +8,13 @@ from mcp_agent.core.exceptions import ModelConfigError
8
8
  from mcp_agent.core.request_params import RequestParams
9
9
  from mcp_agent.llm.augmented_llm_passthrough import PassthroughLLM
10
10
  from mcp_agent.llm.augmented_llm_playback import PlaybackLLM
11
+ from mcp_agent.llm.augmented_llm_silent import SilentLLM
11
12
  from mcp_agent.llm.augmented_llm_slow import SlowLLM
12
13
  from mcp_agent.llm.provider_types import Provider
13
14
  from mcp_agent.llm.providers.augmented_llm_aliyun import AliyunAugmentedLLM
14
15
  from mcp_agent.llm.providers.augmented_llm_anthropic import AnthropicAugmentedLLM
15
16
  from mcp_agent.llm.providers.augmented_llm_azure import AzureOpenAIAugmentedLLM
17
+ from mcp_agent.llm.providers.augmented_llm_bedrock import BedrockAugmentedLLM
16
18
  from mcp_agent.llm.providers.augmented_llm_deepseek import DeepSeekAugmentedLLM
17
19
  from mcp_agent.llm.providers.augmented_llm_generic import GenericAugmentedLLM
18
20
  from mcp_agent.llm.providers.augmented_llm_google_native import GoogleNativeAugmentedLLM
@@ -20,6 +22,7 @@ from mcp_agent.llm.providers.augmented_llm_google_oai import GoogleOaiAugmentedL
20
22
  from mcp_agent.llm.providers.augmented_llm_openai import OpenAIAugmentedLLM
21
23
  from mcp_agent.llm.providers.augmented_llm_openrouter import OpenRouterAugmentedLLM
22
24
  from mcp_agent.llm.providers.augmented_llm_tensorzero import TensorZeroAugmentedLLM
25
+ from mcp_agent.llm.providers.augmented_llm_xai import XAIAugmentedLLM
23
26
  from mcp_agent.mcp.interfaces import AugmentedLLMProtocol
24
27
 
25
28
  # from mcp_agent.workflows.llm.augmented_llm_deepseek import DeekSeekAugmentedLLM
@@ -31,6 +34,7 @@ LLMClass = Union[
31
34
  Type[OpenAIAugmentedLLM],
32
35
  Type[PassthroughLLM],
33
36
  Type[PlaybackLLM],
37
+ Type[SilentLLM],
34
38
  Type[SlowLLM],
35
39
  Type[DeepSeekAugmentedLLM],
36
40
  Type[OpenRouterAugmentedLLM],
@@ -38,6 +42,7 @@ LLMClass = Union[
38
42
  Type[GoogleNativeAugmentedLLM],
39
43
  Type[GenericAugmentedLLM],
40
44
  Type[AzureOpenAIAugmentedLLM],
45
+ Type[BedrockAugmentedLLM],
41
46
  ]
42
47
 
43
48
 
@@ -75,6 +80,7 @@ class ModelFactory:
75
80
  """
76
81
  DEFAULT_PROVIDERS = {
77
82
  "passthrough": Provider.FAST_AGENT,
83
+ "silent": Provider.FAST_AGENT,
78
84
  "playback": Provider.FAST_AGENT,
79
85
  "slow": Provider.FAST_AGENT,
80
86
  "gpt-4o": Provider.OPENAI,
@@ -106,10 +112,17 @@ class ModelFactory:
106
112
  "gemini-2.0-flash": Provider.GOOGLE,
107
113
  "gemini-2.5-flash-preview-05-20": Provider.GOOGLE,
108
114
  "gemini-2.5-pro-preview-05-06": Provider.GOOGLE,
115
+ "grok-4": Provider.XAI,
116
+ "grok-4-0709": Provider.XAI,
117
+ "grok-3": Provider.XAI,
118
+ "grok-3-mini": Provider.XAI,
119
+ "grok-3-fast": Provider.XAI,
120
+ "grok-3-mini-fast": Provider.XAI,
109
121
  "qwen-turbo": Provider.ALIYUN,
110
122
  "qwen-plus": Provider.ALIYUN,
111
123
  "qwen-max": Provider.ALIYUN,
112
124
  "qwen-long": Provider.ALIYUN,
125
+
113
126
  }
114
127
 
115
128
  MODEL_ALIASES = {
@@ -140,16 +153,19 @@ class ModelFactory:
140
153
  Provider.GENERIC: GenericAugmentedLLM,
141
154
  Provider.GOOGLE_OAI: GoogleOaiAugmentedLLM,
142
155
  Provider.GOOGLE: GoogleNativeAugmentedLLM,
156
+ Provider.XAI: XAIAugmentedLLM,
143
157
  Provider.OPENROUTER: OpenRouterAugmentedLLM,
144
158
  Provider.TENSORZERO: TensorZeroAugmentedLLM,
145
159
  Provider.AZURE: AzureOpenAIAugmentedLLM,
146
160
  Provider.ALIYUN: AliyunAugmentedLLM,
161
+ Provider.BEDROCK: BedrockAugmentedLLM,
147
162
  }
148
163
 
149
164
  # Mapping of special model names to their specific LLM classes
150
165
  # This overrides the provider-based class selection
151
166
  MODEL_SPECIFIC_CLASSES: Dict[str, LLMClass] = {
152
167
  "playback": PlaybackLLM,
168
+ "silent": SilentLLM,
153
169
  "slow": SlowLLM,
154
170
  }
155
171
 
@@ -197,6 +213,11 @@ class ModelFactory:
197
213
  # If provider still None, try to get from DEFAULT_PROVIDERS using the model_name_str
198
214
  if provider is None:
199
215
  provider = cls.DEFAULT_PROVIDERS.get(model_name_str)
216
+
217
+ # If still None, try pattern matching for Bedrock models
218
+ if provider is None and BedrockAugmentedLLM.matches_model_pattern(model_name_str):
219
+ provider = Provider.BEDROCK
220
+
200
221
  if provider is None:
201
222
  raise ModelConfigError(
202
223
  f"Unknown model or provider for: {model_string}. Model name parsed as '{model_name_str}'"
@@ -11,12 +11,8 @@ from pydantic import BaseModel
11
11
  from mcp_agent.core.exceptions import ProviderKeyError
12
12
 
13
13
  PROVIDER_ENVIRONMENT_MAP: Dict[str, str] = {
14
- "anthropic": "ANTHROPIC_API_KEY",
15
- "openai": "OPENAI_API_KEY",
16
- "deepseek": "DEEPSEEK_API_KEY",
17
- "google": "GOOGLE_API_KEY",
18
- "openrouter": "OPENROUTER_API_KEY",
19
- "generic": "GENERIC_API_KEY",
14
+ # default behaviour in _get_env_key_name is to capitalize the
15
+ # provider name and suffix "_API_KEY" - so no specific mapping needed unless overriding
20
16
  "huggingface": "HF_TOKEN",
21
17
  }
22
18
  API_KEY_HINT_TEXT = "<your-api-key-here>"
@@ -66,7 +62,14 @@ class ProviderKeyManager:
66
62
  ProviderKeyError: If the API key is not found or is invalid
67
63
  """
68
64
 
65
+ from mcp_agent.llm.provider_types import Provider
66
+
69
67
  provider_name = provider_name.lower()
68
+
69
+ # Fast-agent provider doesn't need external API keys
70
+ if provider_name == "fast-agent":
71
+ return ""
72
+
70
73
  api_key = ProviderKeyManager.get_config_file_key(provider_name, config)
71
74
  if not api_key:
72
75
  api_key = ProviderKeyManager.get_env_var(provider_name)
@@ -75,9 +78,20 @@ class ProviderKeyManager:
75
78
  api_key = "ollama" # Default for generic provider
76
79
 
77
80
  if not api_key:
81
+ # Get proper display name for error message
82
+ try:
83
+ provider_enum = Provider(provider_name)
84
+ display_name = provider_enum.display_name
85
+ except ValueError:
86
+ # Invalid provider name
87
+ raise ProviderKeyError(
88
+ f"Invalid provider: {provider_name}",
89
+ f"'{provider_name}' is not a valid provider name.",
90
+ )
91
+
78
92
  raise ProviderKeyError(
79
- f"{provider_name.title()} API key not configured",
80
- f"The {provider_name.title()} API key is required but not set.\n"
93
+ f"{display_name} API key not configured",
94
+ f"The {display_name} API key is required but not set.\n"
81
95
  f"Add it to your configuration file under {provider_name}.api_key "
82
96
  f"or set the {ProviderKeyManager.get_env_key_name(provider_name)} environment variable.",
83
97
  )
@@ -8,15 +8,23 @@ from enum import Enum
8
8
  class Provider(Enum):
9
9
  """Supported LLM providers"""
10
10
 
11
- ANTHROPIC = "anthropic"
12
- DEEPSEEK = "deepseek"
13
- FAST_AGENT = "fast-agent"
14
- GENERIC = "generic"
15
- GOOGLE_OAI = "googleoai" # For Google through OpenAI libraries
16
- GOOGLE = "google" # For Google GenAI native library
17
- OPENAI = "openai"
18
- OPENROUTER = "openrouter"
19
- TENSORZERO = "tensorzero" # For TensorZero Gateway
20
- AZURE = "azure" # Azure OpenAI Service
21
- ALIYUN = "aliyun" # Aliyun Bailian OpenAI Service
22
- HUGGINGFACE = "huggingface" # For HuggingFace MCP connections
11
+ def __new__(cls, config_name, display_name=None):
12
+ obj = object.__new__(cls)
13
+ obj._value_ = config_name
14
+ obj.display_name = display_name or config_name.title()
15
+ return obj
16
+
17
+ ANTHROPIC = ("anthropic", "Anthropic")
18
+ DEEPSEEK = ("deepseek", "Deepseek")
19
+ FAST_AGENT = ("fast-agent", "FastAgent")
20
+ GENERIC = ("generic", "Generic")
21
+ GOOGLE_OAI = ("googleoai", "GoogleOAI") # For Google through OpenAI libraries
22
+ GOOGLE = ("google", "Google") # For Google GenAI native library
23
+ OPENAI = ("openai", "OpenAI")
24
+ OPENROUTER = ("openrouter", "OpenRouter")
25
+ TENSORZERO = ("tensorzero", "TensorZero") # For TensorZero Gateway
26
+ AZURE = ("azure", "Azure") # Azure OpenAI Service
27
+ ALIYUN = ("aliyun", "Aliyun") # Aliyun Bailian OpenAI Service
28
+ HUGGINGFACE = ("huggingface", "HuggingFace") # For HuggingFace MCP connections
29
+ XAI = ("xai", "XAI") # For xAI Grok models
30
+ BEDROCK = ("bedrock", "Bedrock")
@@ -112,7 +112,9 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
112
112
  and event.delta.type == "text_delta"
113
113
  ):
114
114
  # Use base class method for token estimation and progress emission
115
- estimated_tokens = self._update_streaming_progress(event.delta.text, model, estimated_tokens)
115
+ estimated_tokens = self._update_streaming_progress(
116
+ event.delta.text, model, estimated_tokens
117
+ )
116
118
 
117
119
  # Also check for final message_delta events with actual usage info
118
120
  elif (
@@ -285,7 +287,7 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
285
287
  turn_usage = TurnUsage.from_anthropic(
286
288
  response.usage, model or DEFAULT_ANTHROPIC_MODEL
287
289
  )
288
- self.usage_accumulator.add_turn(turn_usage)
290
+ self._finalize_turn_usage(turn_usage)
289
291
  # self._show_usage(response.usage, turn_usage)
290
292
  except Exception as e:
291
293
  self.logger.warning(f"Failed to track usage: {e}")
@@ -435,6 +437,9 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
435
437
  Override this method to use a different LLM.
436
438
 
437
439
  """
440
+ # Reset tool call counter for new turn
441
+ self._reset_turn_tool_calls()
442
+
438
443
  res = await self._anthropic_completion(
439
444
  message_param=message_param,
440
445
  request_params=request_params,