fast-agent-mcp 0.2.24__py3-none-any.whl → 0.2.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fast-agent-mcp
3
- Version: 0.2.24
3
+ Version: 0.2.26
4
4
  Summary: Define, Prompt and Test MCP enabled Agents and Workflows
5
5
  Author-email: Shaun Smith <fastagent@llmindset.co.uk>
6
6
  License: Apache License
@@ -212,14 +212,15 @@ Requires-Python: >=3.10
212
212
  Requires-Dist: a2a-types>=0.1.0
213
213
  Requires-Dist: aiohttp>=3.11.13
214
214
  Requires-Dist: anthropic>=0.49.0
215
+ Requires-Dist: azure-identity>=1.14.0
215
216
  Requires-Dist: fastapi>=0.115.6
216
- Requires-Dist: mcp>=1.8.0
217
+ Requires-Dist: mcp==1.8.0
217
218
  Requires-Dist: openai>=1.63.2
218
219
  Requires-Dist: opentelemetry-distro>=0.50b0
219
220
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.29.0
220
- Requires-Dist: opentelemetry-instrumentation-anthropic>=0.39.3
221
- Requires-Dist: opentelemetry-instrumentation-mcp>=0.40.3
222
- Requires-Dist: opentelemetry-instrumentation-openai>=0.39.3
221
+ Requires-Dist: opentelemetry-instrumentation-anthropic>=0.39.3; python_version >= '3.10' and python_version < '4.0'
222
+ Requires-Dist: opentelemetry-instrumentation-mcp>=0.40.3; python_version >= '3.10' and python_version < '4.0'
223
+ Requires-Dist: opentelemetry-instrumentation-openai>=0.39.3; python_version >= '3.10' and python_version < '4.0'
223
224
  Requires-Dist: prompt-toolkit>=3.0.50
224
225
  Requires-Dist: pydantic-settings>=2.7.0
225
226
  Requires-Dist: pydantic>=2.10.4
@@ -227,6 +228,8 @@ Requires-Dist: pyyaml>=6.0.2
227
228
  Requires-Dist: rich>=13.9.4
228
229
  Requires-Dist: tensorzero>=2025.4.7
229
230
  Requires-Dist: typer>=0.15.1
231
+ Provides-Extra: azure
232
+ Requires-Dist: azure-identity>=1.14.0; extra == 'azure'
230
233
  Provides-Extra: dev
231
234
  Requires-Dist: anthropic>=0.42.0; extra == 'dev'
232
235
  Requires-Dist: pre-commit>=4.0.1; extra == 'dev'
@@ -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=L_wUWTdqFXaRTBA5tL_j2l_9dufWE_MHHPut5e89lBk,12773
3
+ mcp_agent/config.py,sha256=0a6P_nvr6fhBLMjk1XsfQuS_wVO1dmQ1sJR5RQdf5FA,13321
4
4
  mcp_agent/console.py,sha256=Gjf2QLFumwG1Lav__c07X_kZxxEUSkzV-1_-YbAwcwo,813
5
5
  mcp_agent/context.py,sha256=5pnw78LgezCLeO5Os5dgmLDadwXqw_B4Ojib48XP1s4,7431
6
6
  mcp_agent/context_dependent.py,sha256=QXfhw3RaQCKfscEEBRGuZ3sdMWqkgShz2jJ1ivGGX1I,1455
7
- mcp_agent/event_progress.py,sha256=b1VKlQQF2AgPMb6XHjlJAVoPdx8GuxRTUk2g-4lBNm0,2749
7
+ mcp_agent/event_progress.py,sha256=040lrCCclcOuryi07YGSej25kTQF5_JMXY12Yj-3u1U,2773
8
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
@@ -22,7 +22,7 @@ mcp_agent/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  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
- mcp_agent/cli/commands/check_config.py,sha256=9Ryxo_fLInm3YKdYv46yLrAJgnQtMisGreu6Kkriw2g,16677
25
+ mcp_agent/cli/commands/check_config.py,sha256=KJbXUFx5Qih3lb_r-Fcx_uAjgHhgD7qqPewQtIDofKM,18321
26
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
@@ -53,19 +53,20 @@ mcp_agent/llm/augmented_llm.py,sha256=CqtSGo_QrHE73tz_DHMd0wdt2F41gwuUu5Bue51FNm
53
53
  mcp_agent/llm/augmented_llm_passthrough.py,sha256=zHcctNpwg4EFJvD1x9Eg443SVX-uyzFphLikwF_yVE0,6288
54
54
  mcp_agent/llm/augmented_llm_playback.py,sha256=6L_RWIK__R67oZK7u3Xt3hWy1T2LnHXIO-efqgP3tPw,4177
55
55
  mcp_agent/llm/memory.py,sha256=HQ_c1QemOUjrkY6Z2omE6BG5fXga7y4jN7KCMOuGjPs,3345
56
- mcp_agent/llm/model_factory.py,sha256=h3NJSa0yPa9iiLojEqBhIm9wgEBB46ZBibe44MnskHM,8089
56
+ mcp_agent/llm/model_factory.py,sha256=vKR4wI0cDElMc4JSE-WuLXTarAZsi9-5I7B9wUbI6c4,8218
57
57
  mcp_agent/llm/prompt_utils.py,sha256=yWQHykoK13QRF7evHUKxVF0SpVLN-Bsft0Yixzvn0g0,4825
58
58
  mcp_agent/llm/provider_key_manager.py,sha256=-K_FuibN6hdSnweT32lB8mKTfCARnbja6zYYs0ErTKg,2802
59
- mcp_agent/llm/provider_types.py,sha256=oWwXTlyr6hIzU_QLJ5T-UwxZGo5e4Pjwtahz2cr1PHg,364
59
+ mcp_agent/llm/provider_types.py,sha256=m7vAQA0MSn4iVCoHQYwZ8pK8nW4iVLxp_Ul1JpnXMpY,408
60
60
  mcp_agent/llm/sampling_converter.py,sha256=C7wPBlmT0eD90XWabC22zkxsrVHKCrjwIwg6cG628cI,2926
61
61
  mcp_agent/llm/sampling_format_converter.py,sha256=xGz4odHpOcP7--eFaJaFtUR8eR9jxZS7MnLH6J7n0EU,1263
62
62
  mcp_agent/llm/providers/__init__.py,sha256=heVxtmuqFJOnjjxHz4bWSqTAxXoN1E8twC_gQ_yJpHk,265
63
63
  mcp_agent/llm/providers/anthropic_utils.py,sha256=vYDN5G5jKMhD2CQg8veJYab7tvvzYkDMq8M1g_hUAQg,3275
64
64
  mcp_agent/llm/providers/augmented_llm_anthropic.py,sha256=gK_IvllVBNJUUrSfpgFpdhM-d4liCt0MLq7d2lXS7RI,15510
65
+ mcp_agent/llm/providers/augmented_llm_azure.py,sha256=VPrD6lNrEw6EdYUTa9MDvHDNIPjJU5CG5xnKCM3JYdA,5878
65
66
  mcp_agent/llm/providers/augmented_llm_deepseek.py,sha256=NiZK5nv91ZS2VgVFXpbsFNFYLsLcppcbo_RstlRMd7I,1145
66
67
  mcp_agent/llm/providers/augmented_llm_generic.py,sha256=5Uq8ZBhcFuQTt7koP_5ykolREh2iWu8zKhNbh3pM9lQ,1210
67
68
  mcp_agent/llm/providers/augmented_llm_google.py,sha256=N0a2fphVtkvNYxKQpEX6J4tlO1C_mRw4sw3LBXnrOeI,1130
68
- mcp_agent/llm/providers/augmented_llm_openai.py,sha256=jbLG9t0iuneRPX0Cscim6K48SJEB5vPopDE3IBmJ708,14515
69
+ mcp_agent/llm/providers/augmented_llm_openai.py,sha256=5CFHKayjm-aeCBpohIK3WelAEuX7_LDGZIKnWR_rq-s,14577
69
70
  mcp_agent/llm/providers/augmented_llm_openrouter.py,sha256=V_TlVKm92GHBxYIo6gpvH_6cAaIdppS25Tz6x5T7LW0,2341
70
71
  mcp_agent/llm/providers/augmented_llm_tensorzero.py,sha256=Mol_Wzj_ZtccW-LMw0oFwWUt1m1yfofloay9QYNP23c,20729
71
72
  mcp_agent/llm/providers/multipart_converter_anthropic.py,sha256=t5lHYGfFUacJldnrVtMNW-8gEMoto8Y7hJkDrnyZR-Y,16650
@@ -83,11 +84,12 @@ mcp_agent/logging/logger.py,sha256=l02OGX_c5FOyH0rspd4ZvnkJcbb0FahhUhlh2KI8mqE,1
83
84
  mcp_agent/logging/rich_progress.py,sha256=oY9fjb4Tyw6887v8sgO6EGIK4lnmIoR3NNxhA_-Ln_M,4893
84
85
  mcp_agent/logging/transport.py,sha256=m8YsLLu5T8eof_ndpLQs4gHOzqqEL98xsVwBwDsBfxI,17335
85
86
  mcp_agent/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
+ mcp_agent/mcp/common.py,sha256=DiWLH9rxWvCgkKRsHQehY9mDhQl9gki1-q7LVUflDvI,425
86
88
  mcp_agent/mcp/gen_client.py,sha256=fAVwFVCgSamw4PwoWOV4wrK9TABx1S_zZv8BctRyF2k,3030
87
89
  mcp_agent/mcp/interfaces.py,sha256=PAou8znAl2HgtvfCpLQOZFbKra9F72OcVRfBJbboNX8,6965
88
90
  mcp_agent/mcp/logger_textio.py,sha256=vljC1BtNTCxBAda9ExqNB-FwVNUZIuJT3h1nWmCjMws,3172
89
91
  mcp_agent/mcp/mcp_agent_client_session.py,sha256=4597ww1ihSKh-zKc9xMF3ODqosVPU_A4xVmUbk1DvcE,6002
90
- mcp_agent/mcp/mcp_aggregator.py,sha256=_zqSuWGwRTLleXldQjqPSNqV0RRRr1luJIZOvB2AdRg,46011
92
+ mcp_agent/mcp/mcp_aggregator.py,sha256=KAtQTg6CBpTiHoMg6NKcMSiJ7Cvl-BgS0Lff784qwrs,46063
91
93
  mcp_agent/mcp/mcp_connection_manager.py,sha256=jlqaAdS4zc1UfVBHQU0TkTbVr0-rOkbN9bkrLPrZVLk,17159
92
94
  mcp_agent/mcp/mime_utils.py,sha256=difepNR_gpb4MpMLkBRAoyhDk-AjXUHTiqKvT_VwS1o,1805
93
95
  mcp_agent/mcp/prompt_message_multipart.py,sha256=BDwRdNwyWHb2q2bccDb2iR2VlORqVvkvoG3xYzcMpCE,4403
@@ -145,9 +147,9 @@ mcp_agent/resources/examples/workflows/orchestrator.py,sha256=rOGilFTliWWnZ3Jx5w
145
147
  mcp_agent/resources/examples/workflows/parallel.py,sha256=DQ5vY5-h8Qa5QHcYjsWXhZ_FYrYoloVWOdgeXV9p2gI,1890
146
148
  mcp_agent/resources/examples/workflows/router.py,sha256=E4x_-c3l4YW9w1i4ARcDtkdeqIdbWEGfsMzwLYpdbVc,1677
147
149
  mcp_agent/resources/examples/workflows/short_story.txt,sha256=X3y_1AyhLFN2AKzCKvucJtDgAFIJfnlbsbGZO5bBWu0,1187
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,,
150
+ mcp_agent/ui/console_display.py,sha256=UKqax5V2TC0hkZZORmmd6UqUk0DGX7A25E3h1k9f42k,10982
151
+ fast_agent_mcp-0.2.26.dist-info/METADATA,sha256=5fvMqeH9SLSBslzIQPGrsq1yDv9BU9ksmDq9TApRYJU,30488
152
+ fast_agent_mcp-0.2.26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
153
+ fast_agent_mcp-0.2.26.dist-info/entry_points.txt,sha256=bRniFM5zk3Kix5z7scX0gf9VnmGQ2Cz_Q1Gh7Ir4W00,186
154
+ fast_agent_mcp-0.2.26.dist-info/licenses/LICENSE,sha256=cN3FxDURL9XuzE5mhK9L2paZo82LTfjwCYVT7e3j0e4,10939
155
+ fast_agent_mcp-0.2.26.dist-info/RECORD,,
@@ -92,42 +92,73 @@ def get_secrets_summary(secrets_path: Optional[Path]) -> dict:
92
92
  return result
93
93
 
94
94
 
95
- def check_api_keys(secrets_summary: dict) -> dict:
96
- """Check if API keys are configured in secrets file or environment."""
95
+ def check_api_keys(secrets_summary: dict, config_summary: dict) -> dict:
96
+ """Check if API keys are configured in secrets file or environment, including Azure DefaultAzureCredential.
97
+ Now also checks Azure config in main config file for retrocompatibility.
98
+ """
97
99
  import os
98
100
 
99
- # Initialize results dict using Provider enum values
100
101
  results = {
101
- provider.value: {"env": None, "config": None}
102
+ provider.value: {"env": "", "config": ""}
102
103
  for provider in Provider
103
104
  if provider != Provider.FAST_AGENT
104
- } # Include GENERIC but exclude FAST_AGENT
105
+ }
105
106
 
106
107
  # Get secrets if available
107
108
  secrets = secrets_summary.get("secrets", {})
108
109
  secrets_status = secrets_summary.get("status", "not_found")
110
+ # Get config if available
111
+ config = config_summary if config_summary.get("status") == "parsed" else {}
112
+ config_azure = {}
113
+ if config and "azure" in config.get("config", {}):
114
+ config_azure = config["config"]["azure"]
109
115
 
110
- # Check both environment variables and config file for each provider
111
116
  for provider_value in results:
112
- # Check environment variables using ProviderKeyManager
113
- env_key_name = ProviderKeyManager.get_env_key_name(provider_value)
114
- env_key_value = os.environ.get(env_key_name)
115
- if env_key_value:
116
- # Store the last 5 characters if key is long enough
117
- if len(env_key_value) > 5:
118
- results[provider_value]["env"] = f"...{env_key_value[-5:]}"
119
- else:
120
- results[provider_value]["env"] = "...***"
121
-
122
- # Check secrets file if it was parsed successfully
123
- if secrets_status == "parsed":
124
- config_key = ProviderKeyManager.get_config_file_key(provider_value, secrets)
125
- if config_key and config_key != API_KEY_HINT_TEXT:
126
- # Store the last 5 characters if key is long enough
127
- if len(config_key) > 5:
128
- results[provider_value]["config"] = f"...{config_key[-5:]}"
117
+ # Special handling for Azure: support api_key and DefaultAzureCredential
118
+ if provider_value == "azure":
119
+ # Prefer secrets if present, else fallback to config
120
+ azure_cfg = {}
121
+ if secrets_status == "parsed" and "azure" in secrets:
122
+ azure_cfg = secrets.get("azure", {})
123
+ elif config_azure:
124
+ azure_cfg = config_azure
125
+
126
+ use_default_cred = azure_cfg.get("use_default_azure_credential", False)
127
+ base_url = azure_cfg.get("base_url")
128
+ api_key = azure_cfg.get("api_key")
129
+ # DefaultAzureCredential mode
130
+ if use_default_cred and base_url:
131
+ results[provider_value]["config"] = "DefaultAzureCredential"
132
+ # API key mode (retrocompatible)
133
+ if api_key and api_key != API_KEY_HINT_TEXT:
134
+ if len(api_key) > 5:
135
+ if results[provider_value]["config"]:
136
+ results[provider_value]["config"] += " + api_key"
137
+ else:
138
+ results[provider_value]["config"] = f"...{api_key[-5:]}"
129
139
  else:
130
- results[provider_value]["config"] = "...***"
140
+ if results[provider_value]["config"]:
141
+ results[provider_value]["config"] += " + api_key"
142
+ else:
143
+ results[provider_value]["config"] = "...***"
144
+ else:
145
+ # Check environment variables using ProviderKeyManager
146
+ env_key_name = ProviderKeyManager.get_env_key_name(provider_value)
147
+ env_key_value = os.environ.get(env_key_name)
148
+ if env_key_value:
149
+ if len(env_key_value) > 5:
150
+ results[provider_value]["env"] = f"...{env_key_value[-5:]}"
151
+ else:
152
+ results[provider_value]["env"] = "...***"
153
+
154
+ # Check secrets file if it was parsed successfully
155
+ if secrets_status == "parsed":
156
+ config_key = ProviderKeyManager.get_config_file_key(provider_value, secrets)
157
+ if config_key and config_key != API_KEY_HINT_TEXT:
158
+ if len(config_key) > 5:
159
+ results[provider_value]["config"] = f"...{config_key[-5:]}"
160
+ else:
161
+ results[provider_value]["config"] = "...***"
131
162
 
132
163
  return results
133
164
 
@@ -235,7 +266,7 @@ def show_check_summary() -> None:
235
266
  system_info = get_system_info()
236
267
  config_summary = get_config_summary(config_files["config"])
237
268
  secrets_summary = get_secrets_summary(config_files["secrets"])
238
- api_keys = check_api_keys(secrets_summary)
269
+ api_keys = check_api_keys(secrets_summary, config_summary)
239
270
  fastagent_version = get_fastagent_version()
240
271
 
241
272
  # System info panel
@@ -341,8 +372,10 @@ def show_check_summary() -> None:
341
372
 
342
373
  keys_table.add_row(provider.capitalize(), env_status, config_status, active)
343
374
 
344
- console.print(Panel(keys_table, title="API Keys", border_style="blue"))
345
-
375
+ # Print the API Keys panel (fix: this was missing)
376
+ keys_panel = Panel(keys_table, title="API Keys", border_style="blue", subtitle_align="left")
377
+ console.print(keys_panel)
378
+
346
379
  # MCP Servers panel (shown after API Keys)
347
380
  if config_summary.get("status") == "parsed":
348
381
  mcp_servers = config_summary.get("mcp_servers", [])
@@ -447,4 +480,4 @@ def show(
447
480
  def main(ctx: typer.Context) -> None:
448
481
  """Check and diagnose FastAgent configuration."""
449
482
  if ctx.invoked_subcommand is None:
450
- show_check_summary()
483
+ show_check_summary()
mcp_agent/config.py CHANGED
@@ -179,6 +179,20 @@ class OpenRouterSettings(BaseModel):
179
179
  model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
180
180
 
181
181
 
182
+ class AzureSettings(BaseModel):
183
+ """
184
+ Settings for using Azure OpenAI Service in the fast-agent application.
185
+ """
186
+
187
+ api_key: str | None = None
188
+ resource_name: str | None = None
189
+ azure_deployment: str | None = None
190
+ api_version: str | None = None
191
+ base_url: str | None = None # Optional, can be constructed from resource_name
192
+
193
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
194
+
195
+
182
196
  class OpenTelemetrySettings(BaseModel):
183
197
  """
184
198
  OTEL settings for the fast-agent application.
@@ -302,6 +316,9 @@ class Settings(BaseSettings):
302
316
  tensorzero: Optional[TensorZeroSettings] = None
303
317
  """Settings for using TensorZero inference gateway"""
304
318
 
319
+ azure: AzureSettings | None = None
320
+ """Settings for using Azure OpenAI Service in the fast-agent application"""
321
+
305
322
  logger: LoggerSettings | None = LoggerSettings()
306
323
  """Logger settings for the fast-agent application"""
307
324
 
@@ -19,6 +19,7 @@ class ProgressAction(str, Enum):
19
19
  PLANNING = "Planning"
20
20
  READY = "Ready"
21
21
  CALLING_TOOL = "Calling Tool"
22
+ UPDATED = "Updated"
22
23
  FINISHED = "Finished"
23
24
  SHUTDOWN = "Shutdown"
24
25
  AGGREGATOR_INITIALIZED = "Running"
@@ -10,6 +10,7 @@ from mcp_agent.llm.augmented_llm_passthrough import PassthroughLLM
10
10
  from mcp_agent.llm.augmented_llm_playback import PlaybackLLM
11
11
  from mcp_agent.llm.provider_types import Provider
12
12
  from mcp_agent.llm.providers.augmented_llm_anthropic import AnthropicAugmentedLLM
13
+ from mcp_agent.llm.providers.augmented_llm_azure import AzureOpenAIAugmentedLLM
13
14
  from mcp_agent.llm.providers.augmented_llm_deepseek import DeepSeekAugmentedLLM
14
15
  from mcp_agent.llm.providers.augmented_llm_generic import GenericAugmentedLLM
15
16
  from mcp_agent.llm.providers.augmented_llm_google import GoogleAugmentedLLM
@@ -113,6 +114,7 @@ class ModelFactory:
113
114
  Provider.GOOGLE: GoogleAugmentedLLM, # type: ignore
114
115
  Provider.OPENROUTER: OpenRouterAugmentedLLM,
115
116
  Provider.TENSORZERO: TensorZeroAugmentedLLM,
117
+ Provider.AZURE: AzureOpenAIAugmentedLLM,
116
118
  }
117
119
 
118
120
  # Mapping of special model names to their specific LLM classes
@@ -16,3 +16,4 @@ class Provider(Enum):
16
16
  GENERIC = "generic"
17
17
  OPENROUTER = "openrouter"
18
18
  TENSORZERO = "tensorzero" # For TensorZero Gateway
19
+ AZURE = "azure" # Azure OpenAI Service
@@ -0,0 +1,137 @@
1
+ from openai import AuthenticationError, AzureOpenAI, OpenAI
2
+
3
+ from mcp_agent.core.exceptions import ProviderKeyError
4
+ from mcp_agent.llm.provider_types import Provider
5
+ from mcp_agent.llm.providers.augmented_llm_openai import OpenAIAugmentedLLM
6
+
7
+ try:
8
+ from azure.identity import DefaultAzureCredential
9
+ except ImportError:
10
+ DefaultAzureCredential = None
11
+
12
+
13
+ def _extract_resource_name(url: str) -> str | None:
14
+ from urllib.parse import urlparse
15
+
16
+ host = urlparse(url).hostname or ""
17
+ suffix = ".openai.azure.com"
18
+ return host.replace(suffix, "") if host.endswith(suffix) else None
19
+
20
+
21
+ DEFAULT_AZURE_API_VERSION = "2023-05-15"
22
+
23
+
24
+ class AzureOpenAIAugmentedLLM(OpenAIAugmentedLLM):
25
+ """
26
+ Azure OpenAI implementation extending OpenAIAugmentedLLM.
27
+ Handles both API Key and DefaultAzureCredential authentication.
28
+ """
29
+
30
+ def __init__(self, provider: Provider = Provider.AZURE, *args, **kwargs):
31
+ # Set provider to AZURE, pass through to base
32
+ super().__init__(provider=provider, *args, **kwargs)
33
+
34
+ # Context/config extraction
35
+ context = getattr(self, "context", None)
36
+ config = getattr(context, "config", None) if context else None
37
+ azure_cfg = getattr(config, "azure", None) if config else None
38
+
39
+ if azure_cfg is None:
40
+ raise ProviderKeyError(
41
+ "Missing Azure configuration",
42
+ "Azure provider requires configuration section 'azure' in your config file.",
43
+ )
44
+
45
+ self.use_default_cred = getattr(azure_cfg, "use_default_azure_credential", False)
46
+ default_request_params = getattr(self, "default_request_params", None)
47
+ self.deployment_name = getattr(default_request_params, "model", None) or getattr(
48
+ azure_cfg, "azure_deployment", None
49
+ )
50
+ self.api_version = getattr(azure_cfg, "api_version", None) or DEFAULT_AZURE_API_VERSION
51
+
52
+ if self.use_default_cred:
53
+ self.base_url = getattr(azure_cfg, "base_url", None)
54
+ if not self.base_url:
55
+ raise ProviderKeyError(
56
+ "Missing Azure endpoint",
57
+ "When using 'use_default_azure_credential', 'base_url' is required in azure config.",
58
+ )
59
+ if DefaultAzureCredential is None:
60
+ raise ProviderKeyError(
61
+ "azure-identity not installed",
62
+ "You must install 'azure-identity' to use DefaultAzureCredential authentication.",
63
+ )
64
+ self.credential = DefaultAzureCredential()
65
+
66
+ def get_azure_token():
67
+ token = self.credential.get_token("https://cognitiveservices.azure.com/.default")
68
+ return token.token
69
+
70
+ self.get_azure_token = get_azure_token
71
+ else:
72
+ self.api_key = getattr(azure_cfg, "api_key", None)
73
+ self.resource_name = getattr(azure_cfg, "resource_name", None)
74
+ self.base_url = getattr(azure_cfg, "base_url", None) or (
75
+ f"https://{self.resource_name}.openai.azure.com/" if self.resource_name else None
76
+ )
77
+ if not self.api_key:
78
+ raise ProviderKeyError(
79
+ "Missing Azure OpenAI credentials",
80
+ "Field 'api_key' is required in azure config.",
81
+ )
82
+ if not (self.resource_name or self.base_url):
83
+ raise ProviderKeyError(
84
+ "Missing Azure endpoint",
85
+ "Provide either 'resource_name' or 'base_url' under azure config.",
86
+ )
87
+ if not self.deployment_name:
88
+ raise ProviderKeyError(
89
+ "Missing deployment name",
90
+ "Set 'azure_deployment' in config or pass model=<deployment>.",
91
+ )
92
+ # If resource_name was missing, try to extract it from base_url
93
+ if not self.resource_name and self.base_url:
94
+ self.resource_name = _extract_resource_name(self.base_url)
95
+
96
+ def _openai_client(self) -> OpenAI:
97
+ """
98
+ Returns an AzureOpenAI client, handling both API Key and DefaultAzureCredential.
99
+ """
100
+ try:
101
+ if self.use_default_cred:
102
+ if self.base_url is None:
103
+ raise ProviderKeyError(
104
+ "Missing Azure endpoint",
105
+ "azure_endpoint (base_url) is None at client creation time.",
106
+ )
107
+ return AzureOpenAI(
108
+ azure_ad_token_provider=self.get_azure_token,
109
+ azure_endpoint=self.base_url,
110
+ api_version=self.api_version,
111
+ azure_deployment=self.deployment_name,
112
+ )
113
+ else:
114
+ if self.base_url is None:
115
+ raise ProviderKeyError(
116
+ "Missing Azure endpoint",
117
+ "azure_endpoint (base_url) is None at client creation time.",
118
+ )
119
+ return AzureOpenAI(
120
+ api_key=self.api_key,
121
+ azure_endpoint=self.base_url,
122
+ api_version=self.api_version,
123
+ azure_deployment=self.deployment_name,
124
+ )
125
+ except AuthenticationError as e:
126
+ if self.use_default_cred:
127
+ raise ProviderKeyError(
128
+ "Invalid Azure AD credentials",
129
+ "The configured Azure AD credentials were rejected.\n"
130
+ "Please check your Azure identity setup.",
131
+ ) from e
132
+ else:
133
+ raise ProviderKeyError(
134
+ "Invalid Azure OpenAI API key",
135
+ "The configured Azure OpenAI API key was rejected.\n"
136
+ "Please check that your API key is valid and not expired.",
137
+ ) from e
@@ -78,7 +78,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
78
78
  self._reasoning_effort = self.context.config.openai.reasoning_effort
79
79
 
80
80
  # Determine if we're using a reasoning model
81
- # TODO -- move this to model capabiltities, add o4.
81
+ # TODO -- move this to model capabilities, add o4.
82
82
  chosen_model = self.default_request_params.model if self.default_request_params else None
83
83
  self._reasoning = chosen_model and (
84
84
  chosen_model.startswith("o3") or chosen_model.startswith("o1")
@@ -325,7 +325,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
325
325
  return result
326
326
 
327
327
  def _prepare_api_request(
328
- self, messages, tools, request_params: RequestParams
328
+ self, messages, tools: List[ChatCompletionToolParam] | None, request_params: RequestParams
329
329
  ) -> dict[str, str]:
330
330
  # Create base arguments dictionary
331
331
 
@@ -345,9 +345,8 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
345
345
  )
346
346
  else:
347
347
  base_args["max_tokens"] = request_params.maxTokens
348
-
349
- if tools:
350
- base_args["parallel_tool_calls"] = request_params.parallel_tool_calls
348
+ if tools:
349
+ base_args["parallel_tool_calls"] = request_params.parallel_tool_calls
351
350
 
352
351
  arguments: Dict[str, str] = self.prepare_provider_arguments(
353
352
  base_args, request_params, self.OPENAI_EXCLUDE_FIELDS.union(self.BASE_EXCLUDE_FIELDS)
@@ -356,7 +355,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
356
355
 
357
356
  def adjust_schema(self, inputSchema: Dict) -> Dict:
358
357
  # return inputSchema
359
- if not Provider.OPENAI == self.provider:
358
+ if self.provider not in [Provider.OPENAI, Provider.AZURE]:
360
359
  return inputSchema
361
360
 
362
361
  if "properties" in inputSchema:
@@ -0,0 +1,16 @@
1
+ """
2
+ Common constants and utilities shared between modules to avoid circular imports.
3
+ """
4
+
5
+ # Constants
6
+ SEP = "-"
7
+
8
+
9
+ def create_namespaced_name(server_name: str, resource_name: str) -> str:
10
+ """Create a namespaced resource name from server and resource names"""
11
+ return f"{server_name}{SEP}{resource_name}"
12
+
13
+
14
+ def is_namespaced_name(name: str) -> bool:
15
+ """Check if a name is already namespaced"""
16
+ return SEP in name
@@ -25,6 +25,7 @@ from pydantic import AnyUrl, BaseModel, ConfigDict
25
25
  from mcp_agent.context_dependent import ContextDependent
26
26
  from mcp_agent.event_progress import ProgressAction
27
27
  from mcp_agent.logging.logger import get_logger
28
+ from mcp_agent.mcp.common import SEP, create_namespaced_name, is_namespaced_name
28
29
  from mcp_agent.mcp.gen_client import gen_client
29
30
  from mcp_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
30
31
  from mcp_agent.mcp.mcp_connection_manager import MCPConnectionManager
@@ -35,23 +36,11 @@ if TYPE_CHECKING:
35
36
 
36
37
  logger = get_logger(__name__) # This will be replaced per-instance when agent_name is available
37
38
 
38
- SEP = "-"
39
-
40
39
  # Define type variables for the generalized method
41
40
  T = TypeVar("T")
42
41
  R = TypeVar("R")
43
42
 
44
43
 
45
- def create_namespaced_name(server_name: str, resource_name: str) -> str:
46
- """Create a namespaced resource name from server and resource names"""
47
- return f"{server_name}{SEP}{resource_name}"
48
-
49
-
50
- def is_namespaced_name(name: str) -> bool:
51
- """Check if a name is already namespaced"""
52
- return SEP in name
53
-
54
-
55
44
  class NamespacedTool(BaseModel):
56
45
  """
57
46
  A tool that is namespaced by server name.
@@ -94,6 +83,11 @@ class MCPAggregator(ContextDependent):
94
83
  self._persistent_connection_manager = self.context._connection_manager
95
84
 
96
85
  await self.load_servers()
86
+ # Import the display component here to avoid circular imports
87
+ from mcp_agent.ui.console_display import ConsoleDisplay
88
+
89
+ # Initialize the display component
90
+ self.display = ConsoleDisplay(config=self.context.config)
97
91
 
98
92
  return self
99
93
 
@@ -227,12 +221,11 @@ class MCPAggregator(ContextDependent):
227
221
  write_stream,
228
222
  read_timeout,
229
223
  server_name=server_name,
230
- tool_list_changed_callback=self._handle_tool_list_changed
224
+ tool_list_changed_callback=self._handle_tool_list_changed,
231
225
  )
232
226
 
233
227
  await self._persistent_connection_manager.get_server(
234
- server_name,
235
- client_session_factory=session_factory
228
+ server_name, client_session_factory=session_factory
236
229
  )
237
230
 
238
231
  logger.info(
@@ -282,13 +275,13 @@ class MCPAggregator(ContextDependent):
282
275
  write_stream,
283
276
  read_timeout,
284
277
  server_name=server_name,
285
- tool_list_changed_callback=self._handle_tool_list_changed
278
+ tool_list_changed_callback=self._handle_tool_list_changed,
286
279
  )
287
280
 
288
281
  async with gen_client(
289
282
  server_name,
290
283
  server_registry=self.context.server_registry,
291
- client_session_factory=create_session
284
+ client_session_factory=create_session,
292
285
  ) as client:
293
286
  tools = await fetch_tools(client)
294
287
  prompts = await fetch_prompts(client, server_name)
@@ -923,6 +916,8 @@ class MCPAggregator(ContextDependent):
923
916
  logger.error(f"Cannot refresh tools for unknown server '{server_name}'")
924
917
  return
925
918
 
919
+ await self.display.show_tool_update(aggregator=self, updated_server=server_name)
920
+
926
921
  async with self._refresh_lock:
927
922
  try:
928
923
  # Fetch new tools from the server
@@ -934,12 +929,11 @@ class MCPAggregator(ContextDependent):
934
929
  write_stream,
935
930
  read_timeout,
936
931
  server_name=server_name,
937
- tool_list_changed_callback=self._handle_tool_list_changed
932
+ tool_list_changed_callback=self._handle_tool_list_changed,
938
933
  )
939
934
 
940
935
  server_connection = await self._persistent_connection_manager.get_server(
941
- server_name,
942
- client_session_factory=create_session
936
+ server_name, client_session_factory=create_session
943
937
  )
944
938
  tools_result = await server_connection.session.list_tools()
945
939
  new_tools = tools_result.tools or []
@@ -951,13 +945,13 @@ class MCPAggregator(ContextDependent):
951
945
  write_stream,
952
946
  read_timeout,
953
947
  server_name=server_name,
954
- tool_list_changed_callback=self._handle_tool_list_changed
948
+ tool_list_changed_callback=self._handle_tool_list_changed,
955
949
  )
956
950
 
957
951
  async with gen_client(
958
952
  server_name,
959
953
  server_registry=self.context.server_registry,
960
- client_session_factory=create_session
954
+ client_session_factory=create_session,
961
955
  ) as client:
962
956
  tools_result = await client.list_tools()
963
957
  new_tools = tools_result.tools or []
@@ -5,7 +5,8 @@ from rich.panel import Panel
5
5
  from rich.text import Text
6
6
 
7
7
  from mcp_agent import console
8
- from mcp_agent.mcp.mcp_aggregator import SEP
8
+ from mcp_agent.mcp.common import SEP
9
+ from mcp_agent.mcp.mcp_aggregator import MCPAggregator
9
10
 
10
11
  # Constants
11
12
  HUMAN_INPUT_TOOL_NAME = "__human_input__"
@@ -96,6 +97,32 @@ class ConsoleDisplay:
96
97
  console.console.print(panel, markup=self._markup)
97
98
  console.console.print("\n")
98
99
 
100
+ async def show_tool_update(self, aggregator: MCPAggregator | None, updated_server: str) -> None:
101
+ """Show a tool update for a server"""
102
+ if not self.config or not self.config.logger.show_tools:
103
+ return
104
+
105
+ display_server_list = Text()
106
+
107
+ if aggregator:
108
+ for server_name in await aggregator.list_servers():
109
+ style = "green" if updated_server == server_name else "dim white"
110
+ display_server_list.append(f"[{server_name}] ", style)
111
+
112
+ panel = Panel(
113
+ f"[dim green]Updating tools for server {updated_server}[/]",
114
+ title="[TOOL UPDATE]",
115
+ title_align="left",
116
+ style="green",
117
+ border_style="bold white",
118
+ padding=(1, 2),
119
+ subtitle=display_server_list,
120
+ subtitle_align="left",
121
+ )
122
+ console.console.print("\n")
123
+ console.console.print(panel, markup=self._markup)
124
+ console.console.print("\n")
125
+
99
126
  def _format_tool_list(self, available_tools, selected_tool_name):
100
127
  """Format the list of available tools, highlighting the selected one."""
101
128
  display_tool_list = Text()